feat: add structured remote control commands - #1843
Conversation
|
I just want to say I appreciate you submitting, but right now anything related to shortcuts are not going to be merged largely because this is effectively a UI UX challenge because 100 different people want 100 different features and shortcuts, so we need to figure out a generic solution, not a per-user solution I'm closed because I don't understand your human written description in total. From what it reads like, it seems like it's related to assigning different actions different shortcuts, and right now this is not going to be merged because we are not accepting new features, especially in regards to shortcuts. If you can describe in better detail what exactly this is doing, it would help me review from a human perspective. I don't exactly understand the context that this is coming from. I do understand it's a CLI only change, but I need more context |
|
@cjpais sounds good! I'll explain, but I know you're busy. This is my take at generalizing the CLI so that it can eventually drive all the functionality that Handy can do if a person wanted that. My goal was to allow anyone using it to be able to drive it via CLI. It would look something like the following, and I can refactor it if we even want it but just want different words: That's what this PR suggests. But it also doesn't back Handy into a corner and would allow the following in the future: I wrote more about the idea here and submitted this PR as an example of what the implementation could look like: #211 (comment) Also, if I can help with Handy in general in way that would save you time, let me know. |
|
re-opening for now, I will consider more the only thing is, I do just wonder how much this should be it's own cli rather than handy specifically? handy is not really meant to be a cli |
|
Thanks for reopening. And also, one of the reasons I like Handy is because you are thoughtful about the architecture, and I don't want to mess that up. My engineering brain kept running with this in hopes I can help in some way, so I looked at the applications I use/have used and from what I can tell these are four different ways of doing UI + CLI
The cheapest pattern I can see that doesn't back applications into a corner seems to be:
This is similar to how Handy works today with its existing flags like The only way I can work out a separate CLI in my brain, Handy app would still need a handler and packaging/versioning of some sort, unless the CLI is separate product entirely that doesn't need Handy app installed. ^ those are just my musings from my research. I respect your application and just want to help, and if you like this at all I'm happy to change / extend / harden this PR. |
|
Yeah, I mean I guess more or less just to be clear is handy is really intended to be a desktop application and the CLI that we do offer is largely as a workaround for things on Linux rather than something explicitly supported. And I'm not sure I want to explicitly support a CLI or not because it just adds a lot of surface area to the app and features. So that's the main concern is the CLI that's there is really for extraordinarily experienced users, but really not the average person. And this app, broadly speaking, is targeted at the average person |
|
Makes sense to me. I recommend it to my coworkers who don't want to have to work to get ASR going for that reason, so I'm in agreement there. |
|
Okay, so I want to thank you again for this and I just wanted to update you directly in this thread, which is I am now seriously considering adding the CLI. And in addition, broadly speaking, IPC to handy as well. So other applications potentially can hook into it in a reasonable way. I'm going to be thinking about this, and I'm just curious if you have any thoughts as well. I will probably start taking a deeper look at the PR myself in some time. I don't know if this PR itself will be merged, but certainly if I go implement IPC a CLI will come into focus and I will definitely be adding you as co-author as well And in addition, if this is something that sounds interesting to you to potentially take on in the more full capacity, I'm happy to discuss with you or anyone else who wants to take part in the discussion of getting this implemented and brought into handy because I do think it is potentially useful to a lot of people or opens up possibilities that weren't there before and fundamentally does keep the application fairly simple. And in some ways, I know people have been asking for an API server, but I think IPC potentially solves this better. And if someone really wants to make an HTTP server on top of handy, they can fairly easily do that by using the IPC service. So I think this may be more broadly speaking can have a fair amount of impact, relatively speaking, and can kind of bring handy to a bunch of other applications for free |
|
Thank you! I would definitely be interested in helping! I'll put some thought towards it and offer them; I'm also happy to just help with whatever architectural direction you come up with. Maintaining OSS is hard so I just want to help if I can. IPC is a great idea, and I think it will be a lot easier to maintain / integrate upon than an API server. I'll come up with a couple of reference architectures to seed the discussion (I've found that anything, especially something with flaws, gets the conversation going better than nothing). |
|
Today I thought through a few ways it could work. Feel free to shred it and I can continue to revise if it helps. (note: AI helped me think it through but I discussed / vetted / hashed out / directed. We might decide it's slop but at least it's not vibe slop) Short proposalI suggest a small local connection that other programs on the same computer can use to talk to the running Handy application.
The first version would let a local program:
This would be enough for another local program to show Handy's real status, control recording, and use a completed transcript without pretending to type or reading Handy's database directly. The same foundation could allow more in the future: live partial text, model or profile control, broader history access, audio access, or a separate HTTP bridge. This proposal does not include any of those things yet. Each could be considered later on its own. Main options
Would you be willing to review this direction and tell me what you would simplify or change? I am happy to follow your architectural direction and help with a small proof or the implementation if that would be useful. Detailed proposalGoals
What version 1 would do
What this could allow later, but does not do yetOnce there is a stable local connection, separate future proposals could add:
None of those are included in version 1. This RFC only tries to leave a clean path for them if they are wanted later. Version 1 specifically does not include:
Architectureflowchart LR
Client[Local program] <-->|JSON-RPC messages| IPC[IPC connection layer]
IPC --> Commands[Shared Handy actions]
Commands --> Coordinator[TranscriptionCoordinator]
Commands --> History[HistoryManager]
Coordinator --> Pipeline[Audio and transcription pipeline]
Coordinator --> Events[Application event broadcaster]
History --> Events
Events --> IPC
The IPC connection layer should only handle connections, JSON messages, and the updates each client wants. It should not contain separate recording or history logic. The shared Handy actions should be used by IPC and Handy's own user interface and input paths. Updates should come from real application state changes, not from guesses made by the IPC connection layer. Transport
The connection name should be stable and documented so another program can find it without a separate discovery file. On startup, Handy should:
After a crash, the Unix implementation should test an existing socket before removing it. It should only remove the socket when no live Handy process answers. Windows removes a named pipe when its owner closes the last handle. ProtocolUse JSON-RPC 2.0 over a full-duplex local connection, meaning the same connection carries messages in both directions. Each line contains one complete JSON object. Newlines inside transcript text are escaped by JSON. Initial protocol rules:
Connection setupThe client identifies itself, offers the protocol versions it supports, and asks for event topics. {"jsonrpc":"2.0","id":1,"method":"handy.initialize","params":{"protocol_versions":[1],"client":{"name":"example-integration","version":"0.1.0"},"subscriptions":["state","transcription","errors"]}}Handy selects a protocol version and returns its current real state and supported features. {"jsonrpc":"2.0","id":1,"result":{"protocol_version":1,"server":{"name":"Handy","version":"0.9.5"},"session_id":"01K2ABC","capabilities":{"recording_control":true,"state_events":true,"transcript_fetch":true,"partial_transcripts":false,"configuration":false},"state":{"revision":41,"stage":"idle","operation_id":null}}}The application version above is only an example. Clients should check the negotiated protocol version and capabilities instead of assuming support from the Handy version. Public methods
Public events
State modelHandy has one global microphone and one global transcription pipeline. IPC clients do not own separate recording sessions. stateDiagram-v2
[*] --> Idle
Idle --> Recording: start accepted
Recording --> Processing: stop accepted
Recording --> Idle: cancel
Processing --> Idle: transcription completed
Processing --> Idle: failed or cancelled
All clients observe the same state. If two clients send commands at the same time, the coordinator processes them in arrival order. An operation ID connects later events to the recording that produced them. It does not give one client exclusive control. A recording started from Handy itself should still receive an operation ID and produce the same events. Recording command examplesStart a normal recording: {"jsonrpc":"2.0","id":2,"method":"recording.start","params":{"post_process":false}}{"jsonrpc":"2.0","id":2,"result":{"operation_id":"01K2DEF","state":"recording","changed":true}}Start with post-processing: {"jsonrpc":"2.0","id":3,"method":"recording.start","params":{"post_process":true}}Starting when Handy is already recording is successful but makes no change: {"jsonrpc":"2.0","id":3,"result":{"operation_id":"01K2DEF","state":"recording","changed":false}}Stop the active recording: {"jsonrpc":"2.0","id":4,"method":"recording.stop","params":{}}{"jsonrpc":"2.0","id":4,"result":{"operation_id":"01K2DEF","state":"processing","changed":true}}Cancel the current operation: {"jsonrpc":"2.0","id":5,"method":"recording.cancel","params":{}}
Event and transcript examplesWhen Handy enters processing, subscribed clients receive: {"jsonrpc":"2.0","method":"state.changed","params":{"revision":43,"stage":"processing","operation_id":"01K2DEF"}}When the transcript is ready, subscribed clients receive its ID but not the transcript text: {"jsonrpc":"2.0","method":"transcription.completed","params":{"revision":44,"operation_id":"01K2DEF","transcript_id":123,"post_process_requested":true,"post_process_succeeded":true}}An interested client can then fetch the transcript: {"jsonrpc":"2.0","id":6,"method":"transcript.get","params":{"transcript_id":123}}{"jsonrpc":"2.0","id":6,"result":{"id":123,"created_at":1786500000,"raw_text":"This is the raw transcript.","processed_text":"This is the cleaned transcript.","preferred_text":"This is the cleaned transcript.","post_process_requested":true,"post_process_succeeded":true}}Keeping transcript text out of the completion event has a few benefits:
Complete example flowsequenceDiagram
participant Client as Local program
participant IPC as IPC connection layer
participant App as Shared Handy actions
participant Coord as TranscriptionCoordinator
participant History as HistoryManager
Client->>IPC: handy.initialize
IPC-->>Client: version, capabilities, current state
Client->>IPC: recording.start
IPC->>App: start recording
App->>Coord: apply one state change at a time
Coord-->>App: recording started
IPC-->>Client: accepted, operation ID
IPC-->>Client: state.changed: recording
Client->>IPC: recording.stop
IPC->>App: stop recording
App->>Coord: apply one state change at a time
Coord-->>App: processing started
IPC-->>Client: accepted, processing
IPC-->>Client: state.changed: processing
Coord->>History: save completed transcript
History-->>IPC: transcript available
IPC-->>Client: transcription.completed: transcript ID
Client->>IPC: transcript.get
IPC->>History: fetch transcript ID
History-->>IPC: transcript data
IPC-->>Client: transcript result
ErrorsUse normal JSON-RPC errors with stable Handy error names in {"jsonrpc":"2.0","id":7,"error":{"code":-32001,"message":"Handy is busy processing another recording","data":{"name":"busy","state":"processing","operation_id":"01K2DEF"}}}Suggested application errors:
Unexpected internal errors should include a diagnostic ID so a developer can find the matching log entry. They should not expose transcript text, filesystem paths, or other sensitive details. Events and reconnection
Local trust and safetyThis proposal trusts processes already running as the current operating-system user. It should still prevent accidental exposure outside that user:
Versioning
Implementation boundariesThe design should keep three layers separate:
This keeps the public protocol independent from Tauri commands, Rust type names, database rows, and platform-specific transport details. Suggested implementation order
I am happy to simplify this, build a small proof to test the uncertain parts, or help implement the direction you prefer. Or I can take a whack at a PR. Or whatever -- I'm not trying to enforce this on your project lol |
|
Thanks for this, I'll take a deeper look tomorrow most likely, but I think I've largely come to a similar set of things and may open a PR with some scaffolding over the next day or two and after I do so we can probably move the discussion there itself |
Before Submitting This PR
If this is a feature or change that was previously closed/rejected:
Human Written Description
This is my first time collaborating, but I wanted to say thanks for Handy. I use it on Windows every day. I have an elgator streamdeck, and I was thinking how useful it would be to use it with Handy -- one button toggles recording, another button copies a transcript, another button pastes, etc.
Related Issues/Discussions
Discussion: #211 (comment)
Related PRs: #792 established Handy's existing single-instance CLI controls. #659 addressed a similar latest-transcript retrieval use case through a new global shortcut; this proposal instead keeps bindings outside Handy and adds no shortcut or settings UI.
Community Feedback
This draft is being shared alongside a proposal in Discussion #211 to gather feedback before requesting review. I am happy to narrow or revise the command surface based on maintainer and community direction.
What Changed
--toggle-transcription,--toggle-post-process, and--cancelflags as aliases.Testing
All automated development and verification ran in Docker using Ubuntu 24.04:
cargo test: 171 passedcargo fmt -- --checkcargo clippy --all-targets(only pre-existing warnings remain)bun run check:translationsbun run lintbunx prettier --check .bun run buildWindows runtime and Stream Deck focus/paste behavior have not yet been validated. I would appreciate help using the draft PR's Windows build artifact for that target-platform testing before this is considered ready for review.
Screenshots/Videos (if applicable)
N/A — this is a CLI-only change with no new UI.
AI Assistance
If AI was used: