Yaak Proxy: prose pass over the UI copy

Apply the approved wording table: labels stay short, explanations are one
full sentence about the user's traffic, and detail moves behind an info
icon on the first-run card, the body banners, the Settings Helper row and
the rejected card. One word per thing: traffic and flows, recorded,
truncated, domains, searches, the CLI, Retry.

Backend messages that reach the window change with them, with the tests
and harness fixtures that assert on the old text. The two working files
for the pass are removed from the branch.

Co-Authored-By: Claude Fable 5.2 <noreply@anthropic.com>
This commit is contained in:
Gregory SchierandClaude Fable 5.2 authored and Stetta task tk_mwbotvy7dhs1uqdk committed 2026-11-11 08:41:49 -07:00
1 parent 1f0541f379
commit 35058cc3be
70 files changed
+419 -1312

No files matched your search

-247
View File
@@ -1,247 +0,0 @@
# Yaak Proxy prose pass: the changed rows
Only the rows of `PROSE.md` with a proposed change, by the same numbers. The proposed text is in bold. The Tooltip column is text for a small info icon beside the line, and is empty for most rows. `{braces}` mark a value filled in at run time, and " / " separates the variants of one string. Rows marked "keep" are left out, your own wording among them.
## 1. First-run setup
| # | Current | Proposed | Tooltip | Note |
| --- | --- | --- | --- | --- |
| 1.2 | Two steps, once. Then every app on this Mac sends its traffic here. | **Traffic from every app on this Mac will be recorded.** | | Only one step is the user's. See flag F1. |
| 1.4 | Turn on the system proxy | **Override system proxy** | | One name for the switch. |
| 1.5 | System proxy on | **System proxy overridden** | | |
| 1.6 | The CLI owns the proxy. Take over to turn this on from here. | **Take over from the CLI to change this.** | | The strip above already says where the proxy runs. |
| 1.7 | Another Yaak Proxy window owns the engine. Quit it to use this one. | **Quit the other Yaak Proxy to use this one.** | | See flag F8. |
| 1.8 | Turn On | **Enable** | | Yaak says "Enable" for switches. |
| 1.9 | Try Again | **Retry** | | |
| 1.10 | Encrypting stored traffic, {percent} | **Encrypting recorded traffic, {percent}** | | Same string as 2.9 and 12.1. |
| 1.13 | Traffic captured before this version is being encrypted on disk. Recording starts when it's done. | **Recording will start once encryption finishes.** | | The first sentence repeats the title. Same string as 2.10. |
| 1.14 | The proxy has not started. | **The proxy failed to start.** | | Fallback when there is no reason. Same as 2.17. |
| 1.15 | Recording is paused, so traffic passes through without being stored. | **Recording is paused.** | | The button beside it is Resume Recording. |
| 1.16 | Open a website or send a request, and its flow appears here. | **Open a website or send a request to begin.** | | |
| 1.20 | Only need one terminal or one app? Copy the environment instead. | **To proxy a single terminal instead, copy the environment.** | | The question is cut. |
| 1.22 | Working with an agent? `yaak mcp` gives it the proxy's flows and actions. | **Agents can connect through `yaak mcp`.** | | The question is cut. |
| 1.23 | Captured traffic is encrypted on disk with a key kept in your keychain, so macOS may ask to let Yaak Proxy use it. | **Recorded traffic is encrypted on disk.** | The key is kept in the system keychain, so the system may ask to let Yaak Proxy use it. | The keychain prompt is the detail someone needs. "macOS" showed on every platform; see flag F2. |
## 2. Header and status menu
| # | Current | Proposed | Tooltip | Note |
| --- | --- | --- | --- | --- |
| 2.4 | Clear flows (traffic is locked) | **Clear flows** | | The locked state is on screen. |
| 2.5 | Clear flows (none yet) | **Clear flows** | | |
| 2.9 | Encrypting stored traffic, {percent} | **Encrypting recorded traffic, {percent}** | | |
| 2.10 | Traffic captured before this version is being encrypted on disk. Recording starts when it's done. | **Recording will start once encryption finishes.** | | The first sentence repeats the title. |
| 2.12 | Started by yaak proxy start, pid {pid}, since {time}. Port {port}. | **The proxy was started by the CLI at {time}.** | | The port is the menu's first item, and the pid is on the strip, 2.36. |
| 2.14 | Every flow is stored as it passes. | **(remove)** | | It repeats "Recording" in the title above it. Needs the row to allow no description. |
| 2.15 | Traffic passes through untouched, and nothing is stored. | **Traffic passes through without being recorded.** | | The same sentence as 4.30. |
| 2.17 | The proxy has not started. | **The proxy failed to start.** | | |
| 2.21 | Routing this Mac's web traffic through Yaak Proxy | **Traffic from every app will be recorded.** | | The model sentence. |
| 2.22 | Not routing. Only apps pointed at port {port} are captured | **Only traffic sent to port {port} will be recorded.** | | |
| 2.24 | Waiting for your password | **Waiting for macOS** | | The same progress label as 2.29. |
| 2.25 | Cancelled in the password prompt | **The password prompt was cancelled.** | | See flag D2. |
| 2.26 | macOS asks for your password each time until the helper is approved in Settings. | **macOS will ask for a password each time.** | Install the helper in Settings to stop the prompts. | See flag F3. |
| 2.30 | Except Apple services and apps that pin | **Excluded hosts and apps stay encrypted.** | | See flag F4. |
| 2.32 | Nothing listed | **No hosts or apps** | | An empty-state label beside the count, 2.31. Same as 10.34. |
| 2.35 | Another Yaak Proxy window owns the engine. Quit it to use this one. | **Quit the other copy to use this one.** | | The lead beside it already says where the proxy runs. See flag D1. |
| 2.36 | pid {pid}, since {time}. Flows still list here, read from the capture database. | **pid {pid}, since {time}** | | Metadata, so a fragment. The list below already shows that its traffic is listed. |
| 2.37 | Take over | **Take Over** | | |
| 2.39 | Required to decrypt | **The certificate must be trusted to decrypt traffic.** | | |
| 2.43 | New connections, except Apple services and apps that pin | **New connections will be decrypted, except for excluded hosts and apps.** | | On a text button, so it explains. |
| 2.45 | Choose… | **Choose** | | No ellipsis on a button that opens a menu. |
| 2.47 | Sites | **Domains** | | |
| 2.48 | Edit List in Settings… | **Decryption Settings** | | As Yaak's "Workspace Settings". |
| 2.49 | Not now | **Dismiss** | | As Yaak's toast and banner. |
## 3. Search box and search history
| # | Current | Proposed | Tooltip | Note |
| --- | --- | --- | --- | --- |
| 3.3 | Save search | **Save Search** | | |
| 3.7 | Recent filters | **Recent searches** | | |
| 3.8 | No recent filters | **No recent searches** | | As Yaak's "No recent requests". |
| 3.14 | Not yet | **Not supported** | | |
| 3.19 | App or program | **App** | | |
| 3.21 | A header on either side | **A request or response header** | | |
| 3.29 | Working folder | **Working directory** | | The Info tab says "Directory". |
| 3.30 | Process id | **Process ID** | | |
| 3.32 | Not yet | **Not supported** | | |
## 4. Flow list and its menus
| # | Current | Proposed | Tooltip | Note |
| --- | --- | --- | --- | --- |
| 4.4 | Add Column… | **Add Column** | | |
| 4.9 | Clear Sorting | **Reset Sort** | | The same action as 4.5. |
| 4.15 | Encrypted: passed through after a client rejected the certificate | **Encrypted: an app rejected the certificate** | | |
| 4.16 | Encrypted: not on the decrypt list | **Encrypted: decryption is not enabled** | | Follows your card line. |
| 4.17 | Rejected the proxy's certificate | **Certificate rejected by the app** | | Parallel to "Cancelled by the app". Same as 12.32. |
| 4.19 | Body cut at the capture limit | **Body truncated at the capture limit** | | |
| 4.26 | Export HAR | **Export as HAR** | | As "Export All Flows as HAR". |
| 4.29 | The proxy is not running. Retry from the menu by the state. | **Retry from the status menu to start the proxy.** | | |
| 4.30 | Traffic passes through without being stored. | **Traffic passes through without being recorded.** | | |
| 4.31 | Recording on port {port}. Flows appear as apps send requests. | **Traffic sent to port {port} will be recorded.** | | See flag F10. |
| 4.32 | Paused at {time}. Traffic passes through without being stored. | **Recording was paused at {time}.** | | Without a time: "Recording is paused." The rest is in the status menu, 2.15. |
| 4.33 | No flows match. Found matches for {query} ({n}), or {query} ({n}) | **No results, but found matches for {query} ({n}) or {query} ({n})** | | Yaak's sidebar wording, word for word. Alone: "No results". |
## 5. Sidebar and its menus
| # | Current | Proposed | Tooltip | Note |
| --- | --- | --- | --- | --- |
| 5.1 | All traffic | **All Traffic** | | The menu item that names it is "Show in All Traffic". |
| 5.6 | Decrypt {n} Apps / {n} Hosts / {n} Sites / {n} Selected | **Decrypt {n} Apps / {n} Hosts / {n} Domains / {n} Selected** | | Also in its toast, lower case. |
| 5.8 | Show in Folder | **Show in File Manager** | | Yaak's name on Linux. Also the Settings button, 10.3. |
| 5.9 | Copy path | **Copy Path** | | |
## 6. Detail card and its tabs
| # | Current | Proposed | Tooltip | Note |
| --- | --- | --- | --- | --- |
| 6.6 | Body cut | **Body truncated** | | |
| 6.7 | First {size} kept | **First {size} recorded** | | |
| 6.10 | Cancelled by the client | **Cancelled by the app** | | The list says "app" (4.18). |
| 6.12 | Record | **Flow** | | "Record" is the app's verb. |
| 6.16 | Headers | **Latency** | | The column's name. See flag N3. |
| 6.17 | Total | **Time** | | The column's name. |
| 6.18a | Saved As | **Yaak Request ID** | | Its value is an ID, not a name. See flag F9. |
| 6.24 | {error}. Showing the body as it was on the wire. | **{error}. The body is shown as it was received.** | | The view switch calls this "As received". |
| 6.25 | Showing the first {size} of {size}, the capture limit. The app received all of it. | **The first {size} of {size} is shown.** | The body was truncated at the capture limit. The app received all of it. | |
| 6.26 | Showing the first {size} of the decoded body, the capture limit. The app received all of it. | **The first {size} of the decoded body is shown.** | The body was truncated at the capture limit. The app received all of it. | |
| 6.27 | First {size} of {size} kept | **The first {size} of {size} is shown.** | The body was truncated at the capture limit. The app sent all of it. | The same fact as 6.25, so the same sentence. Its tooltip replaces 6.28. |
| 6.28 | The app sent all of it | **(remove, replaced by 6.27's tooltip)** | | Today the tooltip is on the text itself; it moves to an info icon. |
| 6.39 | No Messages / No Events | **No messages / No events** | | Sentence case, as Yaak's majority. |
| 6.44 | The message is cut at the capture limit. | **The message was truncated at the capture limit.** | | |
| 6.46 | No schema for {service}; reflection failed: {error} | **No schema for {service}. Reflection failed: {error}** | | No semicolons. |
| 6.47 | Did not decode as {type}: {error} | **Failed to decode as {type}: {error}** | | Yaak's error form. |
| 6.48 | The body ended partway through this message, {n} of its {n} bytes in | **The body ended after {n} of this message's {n} bytes.** | | |
| 6.50 | Failed to decompress the message: {error} / Failed to inflate the message: {error} | **Failed to decompress the message: {error}** | | Two words for one failure. |
| 6.51 | Not inflated: an earlier message failed to, which lost the context | **This message was not decompressed because an earlier one failed.** | | |
## 7. Encrypted and rejected cards
| # | Current | Proposed | Tooltip | Note |
| --- | --- | --- | --- | --- |
| 7.4 | Nothing is decrypted yet | **Encrypted Content: Enable decryption to see requests to {host}** | | "Encrypted Content:" is the bold lead, as in 7.3. The same case and buttons as 7.3, so the same line. |
| 7.5 | Not on the decrypt list | **This connection was opened before {entry} was decrypted.** | | Shown when the list does decrypt it now. Takes in 7.6. See flag F5. |
| 7.6 | Opened before {entry} was decrypted / Opened before everything was decrypted | **(remove, merged into 7.5)** | | Needs the two lines merged. |
| 7.7 | Excluded by default / Excluded by {entry} | **This connection is excluded by default. / This connection is excluded by {entry}.** | | The first gives way to 7.8 when the kind is known. |
| 7.8 | Apple service / pins its certificate | **Apple services are excluded by default. / Apps that pin certificates are excluded by default.** | | Replaces 7.7's first sentence when the kind is known, so one sentence shows. Needs the two parts merged. |
| 7.15 | macOS may refuse it / {app} may refuse it | **macOS may reject the connection. / {app} may reject the connection.** | | |
| 7.16 | Asks for your password once | **macOS will ask for a password once.** | | |
| 7.17 | Certificate not trusted | **The certificate is not trusted.** | | A sentence here; the pill and strip keep the state name. |
| 7.19 | {app} rejected the certificate / {app} rejected the certificate for this host | **{app} rejected the certificate. / {app} rejected the certificate for this host.** | | |
| 7.20 | A client rejected the certificate | **An app rejected the certificate.** | | |
| 7.21 | uses its own trust store / likely pinned | **(remove, moved to the tooltip)** | {app} uses its own trust store, so it does not trust the certificate. / {app} likely pins its certificate, so it cannot be decrypted. | The why line, 7.19, stays one sentence. On these cards this replaces the general tooltip, 7.9. |
| 7.23 | Try Again / next connection | **Retry / next connection** | | |
| 7.24 | Then rerun {app} | **Paste it into a terminal, then rerun {app}.** | | |
| 7.25 | Passed through until trusted | **This host stays encrypted until the certificate is trusted.** | | |
| 7.26 | Passed through until relaunch | **This host stays encrypted until Yaak Proxy is relaunched.** | | See flag F12. |
| 7.27 | No longer passed through | **The next connection will be decrypted.** | | |
## 8. Toasts
| # | Current | Proposed | Tooltip | Note |
| --- | --- | --- | --- | --- |
| 8.2 | Except Apple services and apps that pin | **Excluded hosts and apps stay encrypted.** | | See flag F4. |
| 8.6 | {n} open connections stay encrypted | **{n} open connections stay encrypted.** | | A sentence, so it takes its period. |
| 8.9 | Certificate not trusted: {error} | **Failed to trust certificate: {error}** | | Tells it apart from 8.8. |
| 8.11 | Copied the environment. Paste it into a terminal to proxy what runs there. | **Copied the environment. Paste it into a terminal.** | | The instruction stays; what it does is on the button's own row. |
| 8.12 | Copied value | **Copied to clipboard** | | Yaak's one copy toast. |
| 8.13 | Copied cURL / Copied URL / Copied request body / Copied response body | **Copied to clipboard** | | |
| 8.14 | Failed to copy: {error} / Failed to copy value: {error} / Failed to copy {thing}: {error} | **Failed to copy: {error}** | | One wording; the reason stays. |
| 8.17 | Body is binary or truncated; export HAR instead | **The body is binary or truncated. Export as HAR instead.** | | No semicolons. |
| 8.19 | as it was on the wire, since it failed to decode | **as received, since it failed to decode** | | |
| 8.20 | cut at the capture limit | **truncated at the capture limit** | | |
| 8.23 | Exported the flow as HAR | **Exported 1 flow as HAR** | | The same event as 8.22. |
| 8.25 | Saved {name} to {workspace}, in {folder}. Yaak picks it up within a second. | **Saved {name} to {workspace}, in {folder}** | | How fast Yaak shows it is an internal. See flag D3. |
| 8.26 | Captured data reset | **Recorded data reset** | | |
| 8.27 | Failed to show {path}: {error} | **Failed to reveal {path}: {error}** | | |
## 9. Dialogs
| # | Current | Proposed | Tooltip | Note |
| --- | --- | --- | --- | --- |
| 9.1 | Clear traffic? | **Clear Flows** | | Titles are never questions. Matches the button and the shortcut's name; "Clear Traffic" is the alternative. |
| 9.2 | 1 flow will be removed. / {n} flows will be removed. | **Permanently clear 1 flow? / Permanently clear {n} flows?** | | Yaak's confirm pattern, "Permanently delete `x`?", with the button's verb. |
| 9.5 | Save search / Rename search | **Save Search / Rename Search** | | |
| 9.11 | Save as Yaak request | **Save as Yaak Request** | | |
| 9.12 | A new request in Yaak with this flow's method, URL, headers and body. | **A new request will be created in Yaak.** | | The form below shows the method, URL, headers and body. |
| 9.13 | Yaak has no workspaces yet. Create one in Yaak, then save here. | **Create a workspace in Yaak first.** | | |
| 9.16 | Yaak sets these itself when it sends | **Yaak sets these itself when it sends the request.** | | |
| 9.17 | Yaak sets these itself, or they only meant something to the proxy | **Yaak sets these, or they were meant for the proxy.** | | |
| 9.18 | Yaak sets it when it sends / Only meant for the proxy / The body is saved decoded | **Yaak sets it when it sends the request. / It was only meant for the proxy. / The body is saved decoded.** | | |
| 9.22 | form URL-encoded with 1 field / form URL-encoded with {n} fields | **a URL-encoded form with 1 field / a URL-encoded form with {n} fields** | | Reads after "the body as". |
| 9.23 | The Authorization header is saved as captured, in plain text. | **The Authorization header is saved in plain text.** | | The dialog matches on the word "Authorization" to colour it. |
| 9.24 | The body was cut at the capture limit, so only its first {n} bytes are saved. | **Only the first {n} bytes of the body are saved.** | | |
| 9.25 | The body is binary and is saved as text, so bytes that are not UTF-8 are replaced. | **The body is binary and may be altered when saved.** | | |
| 9.26 | The multipart body is saved as text with its Content-Type, not as form fields. | **The multipart body is saved as text, not form fields.** | | |
| 9.27 | Yaak picks it up within a second | **(remove)** | | An internal, and cut from the toast too, 8.25. See flag D3. |
| 9.29 | Save request | **Save Request** | | |
| 9.30 | Saving needs Yaak | **Yaak Required** | | As Yaak's "Credentials Required". |
| 9.31 | This flow becomes a request in one of Yaak's workspaces, and Yaak has not been opened on this Mac. Yaak is free for personal use and opens the request with the same viewers you see here. | **This flow will be saved as a request in one of Yaak's workspaces, but Yaak has not been opened on this computer.** | | The first sentence only, as you asked; nothing promoting Yaak. Shorter still: "Yaak has not been opened on this computer." "This Mac" showed on every platform; see flag F2. |
| 9.32 | Or keep the flow as a file | **The flow can be exported as a file instead.** | | |
| 9.33 | Export HAR | **Export as HAR** | | |
| 9.35 | Turn on Yaak Proxy under Allow in the Background | **Approve Helper** | | The instruction moves to the description. |
| 9.36 | In System Settings > General > Login Items. Newer macOS calls the list Background App Activity. | **Turn on Yaak Proxy in System Settings > General > Login Items.** | | See flag F7. |
## 10. Settings
| # | Current | Proposed | Tooltip | Note |
| --- | --- | --- | --- | --- |
| 10.3 | Reveal in Finder / Show in Explorer / Show in Folder | **Reveal in Finder / Show in Explorer / Show in File Manager** | | |
| 10.5 | Listening, decryption, traffic and terminals | **Configure the port, decryption, recorded traffic, and terminal environment.** | | An imperative sentence, as Yaak's page descriptions are. |
| 10.8 | Restarts the proxy | **Changing the port restarts the proxy.** | | |
| 10.10 | For new connections | **The change applies to new connections.** | | |
| 10.11 | Auto / HTTP/1.1 only | **Automatic / HTTP/1.1 only** | | As Yaak's own HTTP version select. |
| 10.12 | Override system proxy / At launch | **Override system proxy at launch / Traffic from every app will be recorded.** | | "At launch" is the end of the title, not a description. |
| 10.14 | Not installed. Changes use the password prompt. | **The helper is not installed.** | The helper changes the system proxy without a password prompt. | One tooltip for the Helper row, the same in every state but 10.17. |
| 10.15 | Awaiting approval. Changes use the password prompt. | **The helper is awaiting approval.** | The helper changes the system proxy without a password prompt. | |
| 10.16 | Enabled. No password prompts. | **The helper is installed.** | The helper changes the system proxy without a password prompt. | |
| 10.17 | Enabled. One more password prompt puts back your settings. | **The helper is installed.** | One more password prompt is needed to restore the system proxy settings. | Shown while the system proxy is still set the old way. |
| 10.18 | Not in dev builds. Changes use the password prompt. | **The helper is not available in dev builds.** | The helper changes the system proxy without a password prompt. | |
| 10.19 | Not found by macOS. Changes use the password prompt. | **macOS could not find the helper.** | The helper changes the system proxy without a password prompt. | |
| 10.20 | Install / Approve / Unregister | **Install / Approve / Uninstall** | | The pair of Install. |
| 10.24 | Reveal | **Reveal in Finder / Show in Explorer / Show in File Manager** | | The app's other reveal buttons name the place. |
| 10.26 | Except built-in exclusions | **Excluded hosts and apps stay encrypted.** | | The same sentence as 2.30 and 8.2. |
| 10.28 | New connections | **Changes apply to new connections.** | | Takes over from the tooltip, 10.29. |
| 10.29 | Applies to new connections | **(remove)** | | Said twice. See flag D4. |
| 10.31 | Exclude / Host or *.site | **Exclude / Host or *.domain** | | |
| 10.32 | App name or bundle id | **App name or bundle ID** | | |
| 10.33 | Not a host or app | **Invalid host or app** | | As "Invalid value" and "Invalid name". |
| 10.34 | Nothing listed | **No hosts or apps** | | |
| 10.40 | Captured traffic | **Recorded traffic** | | |
| 10.43 | Oldest flows dropped past it | **The oldest flows will be cleared first.** | | |
| 10.46 | Reset captured data | **Reset recorded data** | | |
| 10.47 | Deletes all flows and their keys | **All traffic and its encryption keys will be deleted.** | | |
| 10.48 | Reset… | **Reset** | | |
| 10.49 | Reset captured data? | **Reset Recorded Data** | | Titles are never questions. |
| 10.50 | Every captured flow and the keys that encrypt them are deleted. | **Permanently delete all traffic and its encryption keys?** | | Yaak's confirm pattern. |
| 10.54 | Proxies one terminal instead of the system | **Traffic from one terminal will be recorded.** | | |
| 10.57 | A new Terminal window with these set | **A new Terminal window will open with these variables set.** | | On a text button, so it explains. |
| 10.58 | Not available on this platform yet | **Opening a terminal is only supported on macOS.** | | As the error, 12.30. |
## 12. Errors and the locked state
| # | Current | Proposed | Tooltip | Note |
| --- | --- | --- | --- | --- |
| 12.1 | Encrypting stored traffic, {percent} | **Encrypting recorded traffic, {percent}** | | |
| 12.7 | traffic captured before this version is being encrypted. | **traffic recorded before this version is being encrypted.** | | |
| 12.9 | {12.12}, so the traffic it stored can't be encrypted yet. Take over or quit it, then retry. | **{12.12}. Take over or quit it, then retry.** | | Why it blocks is an internal; what to do stays. |
| 12.10 | The proxy's captured traffic is encrypted, but its key is missing from {folder} | **The encryption key is missing from {folder}.** | | |
| 12.11 | Yaak Proxy stores traffic encrypted and couldn't read its key from the keychain: {reason}. Allow it access, then retry. | **Failed to read the encryption key from the keychain: {reason}** | | The remedy is the line above it on screen, 12.4. Code matches on its opening words, one constant, so safe. |
| 12.13 | Yaak Proxy / the yaak CLI | **Yaak Proxy / the CLI** | | |
| 12.15 | The proxy in {owner} (pid {pid}) stopped but did not let go of it | **Failed to take over from {owner} (pid {pid})** | | Yaak's error form. "It" had nothing to point at. |
| 12.19 | Cancelled in the password dialog | **The password prompt was cancelled.** | | The same sentence as 2.25. See flag D2. |
| 12.20 | The proxy is not running, so there is nothing to route to | **The proxy is not running** | | |
| 12.22 | Failed to remove the helper: {error} | **Failed to uninstall the helper: {error}** | | Matches the button, 10.20. |
| 12.23 | The helper is only available in the installed app, not a dev build | **The helper is not available in dev builds** | | As 10.18. A one-sentence error takes no period, as the kept ones. |
| 12.25 | The helper hung up: {error} | **The helper disconnected: {error}** | | |
| 12.26 | The helper did not answer: {error} | **The helper did not respond: {error}** | | |
| 12.27 | The helper's answer was not understood: {error} | **The helper sent an invalid response: {error}** | | |
| 12.29 | The helper is only available on macOS | **The helper is only supported on macOS** | | One form for all four. |
| 12.30 | Opening a terminal with the proxy set is only supported on macOS so far | **Opening a terminal is only supported on macOS** | | |
| 12.32 | Rejected the proxy's certificate | **Certificate rejected by the app** | | See flag D9. |
| 12.34 | Not a request: the client's TLS handshake failed before it sent one | **The TLS handshake failed before any request was sent** | | |
| 12.38 | A WebSocket has no body after its 101 response. Its frames are its messages. | **A WebSocket has no body** | | Reaches the UI when saving such a body to a file. |
| 12.40 | Failed to delete captured traffic: {error} | **Failed to delete recorded traffic: {error}** | | |
| 12.41 | Failed to {read, record, clear} the filter history: {error} | **Failed to {read, record, clear} the search history: {error}** | | |
| 12.48 | The main window is not open to open Settings over | **Failed to open Settings: the main window is closed** | | |
-616
View File
@@ -1,616 +0,0 @@
# Yaak Proxy prose pass, round one (revised)
Read-only proposal. Nothing in the source has changed. Strike or edit rows by their number (for example "drop 2.14", "4.7: use X instead"), then round two applies what is left. `PROSE-changes.md` beside this file lists only the rows with a proposed change.
The table has 409 rows holding about 645 strings (a row with " / " holds the variants of one string). About 207 strings in 188 rows have a proposed change; the rest are "keep", 12 of them your own wording. Twelve rows carry tooltip text. Five visible strings stay over ten words: 4.33 (Yaak's own sentence), 9.31 (kept as asked), 12.4 (an instruction naming the app and the place), and the errors 12.28 and 12.43.
Order of this file: the table, the terminology decisions, the flags, then the voice guide.
Three rules shape the Proposed column. A label (button, menu item, heading, tab, select option, icon-button tooltip, state name) stays short. Anything that explains (a description, a status line, a card line, a hint, a dialog body) is a complete sentence with a subject and a period. And an explanation that is always visible is one sentence of about ten words, modelled in voice and length on "Traffic from every app will be recorded."
The Tooltip column holds detail someone would need that does not fit that sentence: text for a small info icon beside the line, as the encrypted card's why line has. It is empty for most rows. A visible line that only repeats its title or state name is dropped.
## The table
Paths: `components/`, `lib/` and `hooks/` are under `apps/yaak-proxy/`. `ui/` is `packages/ui/src/components/`. `proxy-lib/` is `crates-proxy/yaak-proxy-lib/src/`. `engine/` is `crates/yaak-proxy/src/`. `tauri/` is `crates-tauri/yaak-app-proxy/src/`. `{braces}` mark a value filled in at run time. A row with " / " holds the variants of one string.
### 1. First-run setup
| # | Where | What | Current | Proposed | Tooltip | Note |
| --- | --- | --- | --- | --- | --- | --- |
| 1.1 | `components/FirstRun.tsx:49` | heading | Set up Yaak Proxy | keep | | |
| 1.2 | `components/FirstRun.tsx:52` | description | Two steps, once. Then every app on this Mac sends its traffic here. | Traffic from every app on this Mac will be recorded. | | Only one step is the user's. See flag F1. |
| 1.3 | `components/FirstRun.tsx:53` | description | Traffic appears here as apps send it. | keep | | Shown where the system proxy is unsupported. |
| 1.4 | `components/FirstRun.tsx:60` | step title | Turn on the system proxy | Override system proxy | | One name for the switch. |
| 1.5 | `components/FirstRun.tsx:60` | step title, done | System proxy on | System proxy overridden | | |
| 1.6 | `components/FirstRun.tsx:64` | step description | The CLI owns the proxy. Take over to turn this on from here. | Take over from the CLI to change this. | | The strip above already says where the proxy runs. |
| 1.7 | `components/FirstRun.tsx:65` | step description | Another Yaak Proxy window owns the engine. Quit it to use this one. | Quit the other Yaak Proxy to use this one. | | See flag F8. |
| 1.8 | `components/FirstRun.tsx:76` | button | Turn On | Enable | | Yaak says "Enable" for switches. |
| 1.9 | `components/FirstRun.tsx:76` | button | Try Again | Retry | | |
| 1.10 | `components/FirstRun.tsx:87` | step title | Encrypting stored traffic, {percent} | Encrypting recorded traffic, {percent} | | Same string as 2.9 and 12.1. |
| 1.11 | `components/FirstRun.tsx:89` | step title | Waiting for traffic | keep | | |
| 1.12 | `components/FirstRun.tsx:90` | step title | See traffic | keep | | |
| 1.13 | `components/FirstRun.tsx:94` | step description | Traffic captured before this version is being encrypted on disk. Recording starts when it's done. | Recording will start once encryption finishes. | | The first sentence repeats the title. Same string as 2.10. |
| 1.14 | `components/FirstRun.tsx:96` | error | The proxy has not started. | The proxy failed to start. | | Fallback when there is no reason. Same as 2.17. |
| 1.15 | `components/FirstRun.tsx:98` | step description | Recording is paused, so traffic passes through without being stored. | Recording is paused. | | The button beside it is Resume Recording. |
| 1.16 | `components/FirstRun.tsx:100` | step description | Open a website or send a request, and its flow appears here. | Open a website or send a request to begin. | | |
| 1.17 | `components/FirstRun.tsx:101` | step description | Flows appear here as apps send requests. | keep | | |
| 1.18 | `components/FirstRun.tsx:111` | button | Retry | keep | | |
| 1.19 | `components/FirstRun.tsx:119` | button | Resume Recording | keep | | |
| 1.20 | `components/FirstRun.tsx:126` | description | Only need one terminal or one app? Copy the environment instead. | To proxy a single terminal instead, copy the environment. | | The question is cut. |
| 1.21 | `components/FirstRun.tsx:134` | button | Copy Environment | keep | | |
| 1.22 | `components/FirstRun.tsx:138` | description | Working with an agent? `yaak mcp` gives it the proxy's flows and actions. | Agents can connect through `yaak mcp`. | | The question is cut. |
| 1.23 | `components/FirstRun.tsx:142` | description | Captured traffic is encrypted on disk with a key kept in your keychain, so macOS may ask to let Yaak Proxy use it. | Recorded traffic is encrypted on disk. | The key is kept in the system keychain, so the system may ask to let Yaak Proxy use it. | The keychain prompt is the detail someone needs. "macOS" showed on every platform; see flag F2. |
### 2. Header and status menu
| # | Where | What | Current | Proposed | Tooltip | Note |
| --- | --- | --- | --- | --- | --- | --- |
| 2.1 | `components/ProxyLayout.tsx:82`, `:154` | tooltip | Toggle sidebar | keep | | Same as Yaak's. |
| 2.2 | `components/ProxyLayout.tsx:106` | tooltip | Change to vertical layout / Change to horizontal layout | keep | | Same as Yaak's. |
| 2.3 | `components/ProxyLayout.tsx:117` | tooltip | Settings | keep | | |
| 2.4 | `components/ClearFlowsButton.tsx:52` | tooltip, disabled | Clear flows (traffic is locked) | Clear flows | | The locked state is on screen. |
| 2.5 | `components/ClearFlowsButton.tsx:54` | tooltip, disabled | Clear flows (none yet) | Clear flows | | |
| 2.6 | `components/ClearFlowsButton.tsx:56-57` | tooltip | Clear flows / Clear flows {hotkey} | keep | | |
| 2.7 | `components/ProxyStatus.tsx:190-206` | status pill | Encrypting / CLI / Recording / Paused / Not running | keep | | |
| 2.8 | `components/ProxyStatus.tsx:126` | menu item, right | Copied | keep | | |
| 2.9 | `components/ProxyStatus.tsx:68` | menu row title | Encrypting stored traffic, {percent} | Encrypting recorded traffic, {percent} | | |
| 2.10 | `components/ProxyStatus.tsx:69` | menu row description | Traffic captured before this version is being encrypted on disk. Recording starts when it's done. | Recording will start once encryption finishes. | | The first sentence repeats the title. |
| 2.11 | `components/ProxyStatus.tsx:74` | menu row title | Running from the CLI | keep | | |
| 2.12 | `components/ProxyStatus.tsx:75` | menu row description | Started by yaak proxy start, pid {pid}, since {time}. Port {port}. | The proxy was started by the CLI at {time}. | | The port is the menu's first item, and the pid is on the strip, 2.36. |
| 2.13 | `components/ProxyStatus.tsx:80` | menu row title | Recording on port {port} / Paused on port {port} | keep | | |
| 2.14 | `components/ProxyStatus.tsx:83` | menu row description | Every flow is stored as it passes. | (remove) | | It repeats "Recording" in the title above it. Needs the row to allow no description. |
| 2.15 | `components/ProxyStatus.tsx:84` | menu row description | Traffic passes through untouched, and nothing is stored. | Traffic passes through without being recorded. | | The same sentence as 4.30. |
| 2.16 | `components/ProxyStatus.tsx:90` | menu row title | Not running | keep | | |
| 2.17 | `components/ProxyStatus.tsx:93` | error | The proxy has not started. | The proxy failed to start. | | |
| 2.18 | `components/ProxyStatus.tsx:106` | button | Retry | keep | | |
| 2.19 | `lib/systemProxy.ts:134` | menu item | Pause Recording / Resume Recording | keep | | |
| 2.20 | `components/ProxyStatus.tsx:152`, `:168` | menu row title | Override system proxy | keep | | |
| 2.21 | `lib/systemProxy.ts:47` | menu row description, on | Routing this Mac's web traffic through Yaak Proxy | Traffic from every app will be recorded. | | The model sentence. |
| 2.22 | `lib/systemProxy.ts:48` | menu row description, off | Not routing. Only apps pointed at port {port} are captured | Only traffic sent to port {port} will be recorded. | | |
| 2.23 | `lib/systemProxy.ts:31` | menu row description, pending | Changing the system proxy | keep | | A progress label, as Yaak's "Cloning...". |
| 2.24 | `lib/systemProxy.ts:31` | menu row description, pending | Waiting for your password | Waiting for macOS | | The same progress label as 2.29. |
| 2.25 | `lib/systemProxy.ts:40` | error | Cancelled in the password prompt | The password prompt was cancelled. | | See flag D2. |
| 2.26 | `components/ProxyStatus.tsx:160` | notice | macOS asks for your password each time until the helper is approved in Settings. | macOS will ask for a password each time. | Install the helper in Settings to stop the prompts. | See flag F3. |
| 2.27 | `components/ProxyStatus.tsx:235`, `:256`, `:267` | menu row title | Decrypt everything | keep | | A row title, so sentence case; the button is "Decrypt Everything". |
| 2.28 | `components/ProxyStatus.tsx:215`, `:236` | pill and description | Certificate not trusted | keep | | A state name. |
| 2.29 | `components/ProxyStatus.tsx:247`, `:259` | button and description | Waiting for macOS / Trust | keep | | |
| 2.30 | `components/ProxyStatus.tsx:261` | menu row description, on | Except Apple services and apps that pin | Excluded hosts and apps stay encrypted. | | See flag F4. |
| 2.31 | `components/ProxyStatus.tsx:262`, `lib/decrypt.ts:226-227` | menu row description, off | {n} apps, {n} hosts | keep | | A count. |
| 2.32 | `components/ProxyStatus.tsx:262` | menu row description, off | Nothing listed | No hosts or apps | | An empty-state label beside the count, 2.31. Same as 10.34. |
| 2.33 | `components/TakeOverBanner.tsx:20` | banner | Running from the CLI / Running in another Yaak Proxy | keep | | |
| 2.34 | `components/TakeOverBanner.tsx:27` | banner, code | yaak proxy start | keep | | |
| 2.35 | `components/TakeOverBanner.tsx:32` | banner | Another Yaak Proxy window owns the engine. Quit it to use this one. | Quit the other copy to use this one. | | The lead beside it already says where the proxy runs. See flag D1. |
| 2.36 | `components/TakeOverBanner.tsx:36` | banner | pid {pid}, since {time}. Flows still list here, read from the capture database. | pid {pid}, since {time} | | Metadata, so a fragment. The list below already shows that its traffic is listed. |
| 2.37 | `components/TakeOverBanner.tsx:47` | button | Take over | Take Over | | |
| 2.38 | `components/HttpsStrip.tsx:58` | banner | Certificate not trusted | keep | | |
| 2.39 | `components/HttpsStrip.tsx:59` | banner | Required to decrypt | The certificate must be trusted to decrypt traffic. | | |
| 2.40 | `components/HttpsStrip.tsx:67` | button | Waiting for macOS / Trust | keep | | |
| 2.41 | `components/HttpsStrip.tsx:120` | banner | HTTPS is encrypted | keep | | |
| 2.42 | `lib/httpsStrip.ts:47` | banner count | {n} connection / {n} connections | keep | | |
| 2.43 | `components/HttpsStrip.tsx:127` | tooltip | New connections, except Apple services and apps that pin | New connections will be decrypted, except for excluded hosts and apps. | | On a text button, so it explains. |
| 2.44 | `components/HttpsStrip.tsx:143` | button | Decrypt Everything | keep (user's wording) | | |
| 2.45 | `components/HttpsStrip.tsx:148` | button | Choose… | Choose | | No ellipsis on a button that opens a menu. |
| 2.46 | `components/HttpsStrip.tsx:74` | menu heading | Apps | keep | | |
| 2.47 | `components/HttpsStrip.tsx:96` | menu heading | Sites | Domains | | |
| 2.48 | `components/HttpsStrip.tsx:112` | menu item | Edit List in Settings… | Decryption Settings | | As Yaak's "Workspace Settings". |
| 2.49 | `components/HttpsStrip.tsx:155` | tooltip | Not now | Dismiss | | As Yaak's toast and banner. |
### 3. Search box and search history
| # | Where | What | Current | Proposed | Tooltip | Note |
| --- | --- | --- | --- | --- | --- | --- |
| 3.1 | `components/SearchBar.tsx:527-528` | placeholder | Search flows / Search {n} flows | keep | | |
| 3.2 | `components/SearchBar.tsx:175` | placeholder, locked | Traffic is locked | keep | | |
| 3.3 | `components/SearchBar.tsx:208` | menu item | Save search | Save Search | | |
| 3.4 | `lib/flowActions.ts:97` | menu item | Export All Flows as HAR / Export Matching Flows as HAR | keep | | |
| 3.5 | `components/SearchBar.tsx:222` | tooltip | Search actions | keep | | |
| 3.6 | `components/SearchBar.tsx:230` | tooltip | Clear search | keep | | |
| 3.7 | `components/SearchBar.tsx:314` | tooltip | Recent filters | Recent searches | | |
| 3.8 | `components/SearchBar.tsx:278` | menu item, empty | No recent filters | No recent searches | | As Yaak's "No recent requests". |
| 3.9 | `components/SearchBar.tsx:290` | tooltip | Save search | keep | | |
| 3.10 | `components/SearchBar.tsx:296` | tooltip | Remove | keep | | |
| 3.11 | `components/SearchBar.tsx:303` | menu item | Clear History | keep | | |
| 3.12 | `components/SearchBar.tsx:327` | completion detail | Recent | keep | | |
| 3.13 | `lib/searchBox.ts:42-43` | lint label | Unknown field / Invalid value | keep | | |
| 3.14 | `lib/searchBox.ts:44` | lint label | Not yet | Not supported | | |
| 3.15 | `proxy-lib/query.rs:86` | completion info | Host, wildcards allowed | keep | | |
| 3.16 | `proxy-lib/query.rs:87` | completion info | URL path, /* for below | keep | | |
| 3.17 | `proxy-lib/query.rs:88` | completion info | GET, POST, … | keep | | |
| 3.18 | `proxy-lib/query.rs:89-90` | completion info | Response status / Content type | keep | | |
| 3.19 | `proxy-lib/query.rs:91` | completion info | App or program | App | | |
| 3.20 | `proxy-lib/query.rs:95` | completion info | error, pending, system, encrypted, rejected, … | keep | | A list of its values. |
| 3.21 | `proxy-lib/query.rs:98` | completion info | A header on either side | A request or response header | | |
| 3.22 | `proxy-lib/query.rs:99-100` | completion info | A request header / A response header | keep | | |
| 3.23 | `proxy-lib/query.rs:101-103` | completion info | Response body size / Request body size / Response body size | keep | | |
| 3.24 | `proxy-lib/query.rs:104` | completion info | Total time | keep | | See flag N3. |
| 3.25 | `proxy-lib/query.rs:105` | completion info | When the flow started | keep | | |
| 3.26 | `proxy-lib/query.rs:106` | completion info | The whole URL | keep | | |
| 3.27 | `proxy-lib/query.rs:107` | completion info | A query parameter | keep | | |
| 3.28 | `proxy-lib/query.rs:108-111` | completion info | http or https / Port / h1, h2, ws, sse, grpc / Flow number | keep | | |
| 3.29 | `proxy-lib/query.rs:112` | completion info | Working folder | Working directory | | The Info tab says "Directory". |
| 3.30 | `proxy-lib/query.rs:113` | completion info | Process id | Process ID | | |
| 3.31 | `proxy-lib/query.rs:117-121` | completion info | GraphQL operation name / gRPC service / gRPC method | keep | | |
| 3.32 | `proxy-lib/query.rs:122-124` | completion info | Not yet | Not supported | | |
| 3.33 | `components/AddColumnDialog.tsx:105`, `:110` | input label and placeholder | Field / @header.x-request-id | keep | | The dialog itself is 9.9. |
| 3.34 | `lib/columns.ts:107-110` | validation | Missing name / Invalid name / Incomplete / Unknown field | keep | | |
### 4. Flow list and its menus
| # | Where | What | Current | Proposed | Tooltip | Note |
| --- | --- | --- | --- | --- | --- | --- |
| 4.1 | `lib/columns.ts:66-82` | column headers | App, Method, Host, Path, Query, Status, Type, Request Size, Size, Time, Latency, Started, Remote Address, HTTP Version, TLS Version, Operation | keep | | See flag N3 for Time and Latency. |
| 4.2 | `components/FlowListHeader.tsx:133`, `:251` | menu heading and tooltip | Columns | keep | | |
| 4.3 | `components/FlowListHeader.tsx:152`, `:208` | menu item | Remove Column | keep | | |
| 4.4 | `components/FlowListHeader.tsx:162` | menu item | Add Column… | Add Column | | |
| 4.5 | `components/FlowListHeader.tsx:164` | menu item | Reset Sort | keep (user's wording) | | |
| 4.6 | `components/FlowListHeader.tsx:165`, `:215` | menu item | Reset Column Widths | keep | | |
| 4.7 | `components/FlowListHeader.tsx:167` | menu item | Reset Column Order | keep | | |
| 4.8 | `components/FlowListHeader.tsx:178-181` | menu item | Sort Ascending / Sort Descending / Add to Sort Ascending / Add to Sort Descending | keep | | |
| 4.9 | `components/FlowListHeader.tsx:182` | menu item | Clear Sorting | Reset Sort | | The same action as 4.5. |
| 4.10 | `components/FlowListHeader.tsx:185`, `:190` | menu item | Move Left / Move Right | keep | | |
| 4.11 | `components/FlowListHeader.tsx:196`, `:201` | menu item | Hide Column / Hide Other Columns | keep | | |
| 4.12 | `components/FlowList.tsx:531` | divider | {n} new | keep | | |
| 4.13 | `components/FlowList.tsx:551` | button | 1 new flow / {n} new flows | keep | | |
| 4.14 | `components/FlowList.tsx:914-915` | tooltip | Encrypted: excluded by default / Encrypted: excluded by {entry} | keep | | |
| 4.15 | `components/FlowList.tsx:917` | tooltip | Encrypted: passed through after a client rejected the certificate | Encrypted: an app rejected the certificate | | |
| 4.16 | `components/FlowList.tsx:919` | tooltip | Encrypted: not on the decrypt list | Encrypted: decryption is not enabled | | Follows your card line. |
| 4.17 | `components/FlowList.tsx:932` | tooltip | Rejected the proxy's certificate | Certificate rejected by the app | | Parallel to "Cancelled by the app". Same as 12.32. |
| 4.18 | `components/FlowList.tsx:933-936` | tooltip | Failed: {error} / Cancelled by the app / Streaming | keep | | |
| 4.19 | `components/FlowList.tsx:937` | tooltip | Body cut at the capture limit | Body truncated at the capture limit | | |
| 4.20 | `components/FlowList.tsx:938` | tooltip | Saved to Yaak | keep | | |
| 4.21 | `components/FlowList.tsx:1109`, `:1209` | cell | Rejected certificate / Cancelled | keep | | |
| 4.22 | `components/FlowList.tsx:1361-1367` | menu item | Filter by This Value / Exclude This Value / Copy Value | keep (user's wording) | | |
| 4.23 | `lib/flowActions.ts:148` | menu item | Save as Yaak Request / Save to Yaak Again | keep | | |
| 4.24 | `lib/flowActions.ts:155-193` | menu item | Copy as cURL / Copy URL / Copy Request Body / Copy Response Body | keep | | |
| 4.25 | `lib/flowActions.ts:200`, `:206` | menu item | Save Request Body to File / Save Response Body to File | keep | | |
| 4.26 | `lib/flowActions.ts:212` | menu item | Export HAR | Export as HAR | | As "Export All Flows as HAR". |
| 4.27 | `components/FlowListEmpty.tsx:31` | empty state | No flows yet | keep | | |
| 4.28 | `components/FlowListEmpty.tsx:31` | empty state | Recording is paused | keep | | |
| 4.29 | `components/FlowListEmpty.tsx:34` | empty state | The proxy is not running. Retry from the menu by the state. | Retry from the status menu to start the proxy. | | |
| 4.30 | `components/FlowListEmpty.tsx:36` | empty state | Traffic passes through without being stored. | Traffic passes through without being recorded. | | |
| 4.31 | `components/FlowListEmpty.tsx:37` | empty state | Recording on port {port}. Flows appear as apps send requests. | Traffic sent to port {port} will be recorded. | | See flag F10. |
| 4.32 | `components/FlowListEmpty.tsx:59-60` | list row | Paused at {time}. Traffic passes through without being stored. | Recording was paused at {time}. | | Without a time: "Recording is paused." The rest is in the status menu, 2.15. |
| 4.33 | `components/FlowListEmpty.tsx:105-106` | empty state | No flows match. Found matches for {query} ({n}), or {query} ({n}) | No results, but found matches for {query} ({n}) or {query} ({n}) | | Yaak's sidebar wording, word for word. Alone: "No results". |
| 4.34 | `components/FlowListEmpty.tsx:111` | empty state, code | all traffic | keep | | |
### 5. Sidebar and its menus
| # | Where | What | Current | Proposed | Tooltip | Note |
| --- | --- | --- | --- | --- | --- | --- |
| 5.1 | `lib/sidebarTree.ts:350` | row | All traffic | All Traffic | | The menu item that names it is "Show in All Traffic". |
| 5.2 | `lib/sidebarTree.ts:357`, `:301-303`, `:395` | rows | Saved / Apps / Domains / System / Encrypted | keep | | |
| 5.3 | `components/Sidebar.tsx:103` | tooltip | Encrypted | keep | | |
| 5.4 | `components/Sidebar.tsx:198-201` | menu item | Filter by This Value / Exclude This Value / Copy Value | keep (user's wording) | | |
| 5.5 | `lib/decrypt.ts:395` | menu item | Decrypt {name} / Stop Decrypting {name} | keep | | |
| 5.6 | `lib/decrypt.ts:425-427` | menu item, several rows | Decrypt {n} Apps / {n} Hosts / {n} Sites / {n} Selected | Decrypt {n} Apps / {n} Hosts / {n} Domains / {n} Selected | | Also in its toast, lower case. |
| 5.7 | `lib/opener.ts:26-27` | menu item | Reveal in Finder / Show in Explorer | keep | | |
| 5.8 | `lib/opener.ts:28` | menu item | Show in Folder | Show in File Manager | | Yaak's name on Linux. Also the Settings button, 10.3. |
| 5.9 | `components/Sidebar.tsx:178` | menu item | Copy path | Copy Path | | |
| 5.10 | `components/Sidebar.tsx:225`, `:248` | menu item | Show in All Traffic | keep | | |
| 5.11 | `components/Sidebar.tsx:235` | menu item | Decrypt Everything | keep (user's wording) | | |
| 5.12 | `components/Sidebar.tsx:261`, `:270` | menu item | Rename / Delete | keep | | |
### 6. Detail card and its tabs
| # | Where | What | Current | Proposed | Tooltip | Note |
| --- | --- | --- | --- | --- | --- | --- |
| 6.1 | `components/FlowDetail.tsx:31-35` | empty state | Select a flow / Pause or resume recording / Clear flows | keep | | The keys beside them are fixed to macOS. See flag F6. |
| 6.2 | `components/FlowCard.tsx:144`, `:153`, `:225` | tab | Cookies / Info / Body | keep | | |
| 6.3 | `lib/messages.ts:124` | tab | Events / Messages | keep | | |
| 6.4 | `lib/flowViews.ts:240-248` | tab | Auth / Basic / Bearer / Digest / Negotiate | keep | | |
| 6.5 | `components/FlowCard.tsx:308` | empty state | Waiting for a response | keep | | |
| 6.6 | `components/FlowCard.tsx:326` | badge | Body cut | Body truncated | | |
| 6.7 | `components/FlowCard.tsx:326` | badge | First {size} kept | First {size} recorded | | |
| 6.8 | `components/FlowCard.tsx:336` | tooltip | Save to Yaak / Saved to Yaak | keep | | |
| 6.9 | `components/FlowCard.tsx:341`, `components/EncryptedCard.tsx:298` | tooltip | Actions | keep | | |
| 6.10 | `lib/flowViews.ts:122` | error | Cancelled by the client | Cancelled by the app | | The list says "app" (4.18). |
| 6.11 | `components/FlowInfo.tsx:31-72` | section heading | Connection / App / Timing / Size | keep | | |
| 6.12 | `components/FlowInfo.tsx:78` | section heading | Record | Flow | | "Record" is the app's verb. |
| 6.13 | `components/FlowInfo.tsx:33-38` | row label | Remote Address / Client Version / Server Version / TLS / Cipher / Client Port | keep | | Client and Server as the two HTTP sides. |
| 6.14 | `components/FlowInfo.tsx:45-58` | row label | Name / Bundle ID / Path / PID / Directory | keep | | |
| 6.15 | `components/FlowInfo.tsx:65` | row label | Started | keep | | |
| 6.16 | `components/FlowInfo.tsx:68` | row label | Headers | Latency | | The column's name. See flag N3. |
| 6.17 | `components/FlowInfo.tsx:69` | row label | Total | Time | | The column's name. |
| 6.18 | `components/FlowInfo.tsx:74-81` | row label | Request / Response / Number / ID | keep | | |
| 6.18a | `components/FlowInfo.tsx:82` | row label | Saved As | Yaak Request ID | | Its value is an ID, not a name. See flag F9. |
| 6.19 | `components/FlowInfo.tsx:103` | section heading | Timeline | keep | | |
| 6.20 | `lib/phases.ts:37-58` | timeline step | Started / Connected / TLS handshake / Request sent / Headers received, {status} / Failed / Cancelled / Body received / Closed | keep | | |
| 6.21 | `lib/body.ts:222-247` | tab badge | GraphQL / Binary / Multipart / Form / Image / Audio / Video / Font / Markdown / Protobuf / Text | keep | | |
| 6.22 | `lib/body.ts:44` | empty state | No body. See Messages. | keep | | |
| 6.23 | `lib/body.ts:45` | empty state | No request body / Empty | keep | | Same as Yaak's panes. |
| 6.24 | `components/FlowBodyViewer.tsx:241` | banner | {error}. Showing the body as it was on the wire. | {error}. The body is shown as it was received. | | The view switch calls this "As received". |
| 6.25 | `components/FlowBodyViewer.tsx:247` | banner | Showing the first {size} of {size}, the capture limit. The app received all of it. | The first {size} of {size} is shown. | The body was truncated at the capture limit. The app received all of it. | |
| 6.26 | `components/FlowBodyViewer.tsx:248` | banner | Showing the first {size} of the decoded body, the capture limit. The app received all of it. | The first {size} of the decoded body is shown. | The body was truncated at the capture limit. The app received all of it. | |
| 6.27 | `components/FlowRequestBody.tsx:47` | banner | First {size} of {size} kept | The first {size} of {size} is shown. | The body was truncated at the capture limit. The app sent all of it. | The same fact as 6.25, so the same sentence. Its tooltip replaces 6.28. |
| 6.28 | `components/FlowRequestBody.tsx:45` | tooltip | The app sent all of it | (remove, replaced by 6.27's tooltip) | | Today the tooltip is on the text itself; it moves to an info icon. |
| 6.29 | `components/FlowBodyViewer.tsx:276-277` | label | Binary body, {size}, first {size} shown | keep | | As Yaak's "Binary body, {size}". |
| 6.30 | `components/FlowBodyViewer.tsx:285` | button | Save to File | keep | | |
| 6.31 | `ui/http/BodyViewModeSwitch.tsx:10-17` | switch labels | Pretty / Table / Parts / Preview / Form / Raw | keep | | Proxy only. |
| 6.32 | `ui/http/BodyViewModeSwitch.tsx:10-17` | tooltip | Formatted / As a table / Split into parts / Rendered / As fields / As received | keep | | |
| 6.33 | `ui/http/ResponseCookies.tsx:123`, `:149`, `:173`, `:227` | headings and empty state | Sent Cookies / Received Cookies / Deleted / No Cookies | keep | | Proxy only. |
| 6.34 | `components/FlowMessages.tsx:15-33` | column headers | Type / Message / Event / Data | keep | | |
| 6.35 | `components/FlowMessages.tsx:52` | button | {n} new messages / {n} new events | keep | | |
| 6.36 | `components/FlowMessages.tsx:94-98` | status | CONNECTED / ERROR / CLOSED | keep | | As Yaak's WebSocket pane. |
| 6.37 | `components/FlowMessages.tsx:104` | count | {n} Messages / {n} Events | keep | | |
| 6.38 | `components/FlowMessages.tsx:123` | empty state | Waiting for the first message / Waiting for the first event | keep | | |
| 6.39 | `components/FlowMessages.tsx:123` | empty state | No Messages / No Events | No messages / No events | | Sentence case, as Yaak's majority. |
| 6.40 | `components/FlowMessages.tsx:79-81` | view options | Decoded / Schemaless / Raw bytes | keep | | |
| 6.41 | `components/FlowMessages.tsx:71` | note | Decoded as {type} | keep | | |
| 6.42 | `lib/messages.ts:38`, `:46` | note | No status / No status code | keep | | |
| 6.43 | `lib/messages.ts:66-82` | detail title | Event Received / Closed by the App / Closed by the Server / Ping / Pong / Call Status | keep | | |
| 6.44 | `ui/events/EventStreamViewer.tsx:484` | banner | The message is cut at the capture limit. | The message was truncated at the capture limit. | | |
| 6.45 | `proxy-lib/grpc.rs:370-388` | note | No schema for {service} on {host} | keep | | |
| 6.46 | `proxy-lib/grpc.rs:390` | note | No schema for {service}; reflection failed: {error} | No schema for {service}. Reflection failed: {error} | | No semicolons. |
| 6.47 | `proxy-lib/grpc.rs:454` | note | Did not decode as {type}: {error} | Failed to decode as {type}: {error} | | Yaak's error form. |
| 6.48 | `engine/grpc.rs:187` | message error | The body ended partway through this message, {n} of its {n} bytes in | The body ended after {n} of this message's {n} bytes. | | |
| 6.49 | `engine/grpc.rs:229`, `:240` | message error | Compressed with {encoding}, not decoded / Too large to decompress | keep | | |
| 6.50 | `engine/grpc.rs:241`, `engine/ws.rs:351`, `:399` | message error | Failed to decompress the message: {error} / Failed to inflate the message: {error} | Failed to decompress the message: {error} | | Two words for one failure. |
| 6.51 | `engine/ws.rs:357` | message error | Not inflated: an earlier message failed to, which lost the context | This message was not decompressed because an earlier one failed. | | |
### 7. Encrypted and rejected cards
| # | Where | What | Current | Proposed | Tooltip | Note |
| --- | --- | --- | --- | --- | --- | --- |
| 7.1 | `components/EncryptedCard.tsx:278` | status | ENCRYPTED / REJECTED | keep | | |
| 7.2 | `components/EncryptedCard.tsx:282` | status | open {duration} | keep | | |
| 7.3 | `lib/encryptedCard.ts:276-278` | why line | **Encrypted Content:** Enable decryption to see requests to {host} | keep (user's wording) | | |
| 7.4 | `lib/encryptedCard.ts:272` | why line | Nothing is decrypted yet | **Encrypted Content:** Enable decryption to see requests to {host} | | The same case and buttons as 7.3, so the same line. |
| 7.5 | `lib/encryptedCard.ts:274` | why line | Not on the decrypt list | This connection was opened before {entry} was decrypted. | | Shown when the list does decrypt it now. Takes in 7.6. See flag F5. |
| 7.6 | `lib/encryptedCard.ts:177` | fact line | Opened before {entry} was decrypted / Opened before everything was decrypted | (remove, merged into 7.5) | | Needs the two lines merged. |
| 7.7 | `lib/encryptedCard.ts:266`, `:268` | why line | Excluded by default / Excluded by {entry} | This connection is excluded by default. / This connection is excluded by {entry}. | | The first gives way to 7.8 when the kind is known. |
| 7.8 | `lib/encryptedCard.ts:265` | why line, after | Apple service / pins its certificate | Apple services are excluded by default. / Apps that pin certificates are excluded by default. | | Replaces 7.7's first sentence when the kind is known, so one sentence shows. Needs the two parts merged. |
| 7.9 | `components/EncryptedCard.tsx:178` | tooltip | HTTPS is encrypted between the app and the server. Yaak Proxy shows requests only for hosts and apps you decrypt, from their next connection on. | keep | | |
| 7.10 | `lib/encryptedCard.ts:292`, `:304` | button | Decrypt {name} / Stop Decrypting {name} | keep | | |
| 7.11 | `lib/encryptedCard.ts:209` | button | Decrypt Everything | keep (user's wording) | | |
| 7.12 | `lib/encryptedCard.ts:226-227` | button | Stop Excluding {name} / Stop Excluding {n} Entries | keep | | |
| 7.13 | `lib/encryptedCard.ts:168` | button | Reconnect | keep | | |
| 7.14 | `components/EncryptedCard.tsx:150` | button | Waiting for macOS | keep | | |
| 7.15 | `lib/encryptedCard.ts:197` | foot | macOS may refuse it / {app} may refuse it | macOS may reject the connection. / {app} may reject the connection. | | |
| 7.16 | `lib/encryptedCard.ts:240` | foot | Asks for your password once | macOS will ask for a password once. | | |
| 7.17 | `lib/encryptedCard.ts:379` | why line | Certificate not trusted | The certificate is not trusted. | | A sentence here; the pill and strip keep the state name. |
| 7.18 | `lib/encryptedCard.ts:383` | button | Trust Certificate | keep | | |
| 7.19 | `lib/encryptedCard.ts:394`, `:415` | why line | {app} rejected the certificate / {app} rejected the certificate for this host | {app} rejected the certificate. / {app} rejected the certificate for this host. | | |
| 7.20 | `lib/encryptedCard.ts:416` | why line | A client rejected the certificate | An app rejected the certificate. | | |
| 7.21 | `lib/encryptedCard.ts:395`, `:418` | why line, after | uses its own trust store / likely pinned | (remove, moved to the tooltip) | {app} uses its own trust store, so it does not trust the certificate. / {app} likely pins its certificate, so it cannot be decrypted. | The why line, 7.19, stays one sentence. On these cards this replaces the general tooltip, 7.9. |
| 7.22 | `lib/encryptedCard.ts:400-401` | button and hint | Copy Environment / SSL_CERT_FILE, … | keep | | |
| 7.23 | `lib/encryptedCard.ts:373` | button and hint | Try Again / next connection | Retry / next connection | | |
| 7.24 | `lib/encryptedCard.ts:408` | foot | Then rerun {app} | Paste it into a terminal, then rerun {app}. | | |
| 7.25 | `lib/encryptedCard.ts:386` | foot | Passed through until trusted | This host stays encrypted until the certificate is trusted. | | |
| 7.26 | `lib/encryptedCard.ts:424` | foot | Passed through until relaunch | This host stays encrypted until Yaak Proxy is relaunched. | | See flag F12. |
| 7.27 | `lib/encryptedCard.ts:374` | foot | No longer passed through | The next connection will be decrypted. | | |
### 8. Toasts
A toast's first line is a label and stays a fragment, as Yaak's "Copied to clipboard". Its second line explains, so it is a sentence.
| # | Where | What | Current | Proposed | Tooltip | Note |
| --- | --- | --- | --- | --- | --- | --- |
| 8.1 | `components/ProxyStatus.tsx:276`, `components/HttpsStrip.tsx:138`, `components/Sidebar.tsx:241`, `lib/encryptedCard.ts:212`, `components/DecryptListEditor.tsx:84` | toast | Decrypting everything | keep | | |
| 8.2 | `components/ProxyStatus.tsx:277`, `components/HttpsStrip.tsx:139`, `components/Sidebar.tsx:242`, `lib/encryptedCard.ts:212`, `components/DecryptListEditor.tsx:85` | toast, second line | Except Apple services and apps that pin | Excluded hosts and apps stay encrypted. | | See flag F4. |
| 8.3 | `components/ProxyStatus.tsx:279`, `components/DecryptListEditor.tsx:84` | toast | Stopped decrypting everything | keep | | |
| 8.4 | `components/HttpsStrip.tsx:92`, `:106`, `components/Sidebar.tsx:303`, `lib/encryptedCard.ts:295`, `:308`, `components/DecryptListEditor.tsx:70`, `:103` | toast | Decrypting {name} / Stopped decrypting {name} | keep | | |
| 8.5 | `components/DecryptListEditor.tsx:69`, `:102`, `lib/encryptedCard.ts:234` | toast | Excluding {name} / Stopped excluding {name} | keep | | |
| 8.6 | `lib/decryptChoice.ts:84` | toast, second line | {n} open connections stay encrypted | {n} open connections stay encrypted. | | A sentence, so it takes its period. |
| 8.7 | `hooks/useDecrypt.tsx:151`, `:162` | toast button | Undo / Reconnect | keep | | |
| 8.8 | `hooks/useDecrypt.tsx:123` | warning toast | Certificate not trusted | keep | | |
| 8.9 | `hooks/useDecrypt.tsx:49` | error toast | Certificate not trusted: {error} | Failed to trust certificate: {error} | | Tells it apart from 8.8. |
| 8.10 | `components/Sidebar.tsx:294` | notice toast | Excluded by {entry} | keep | | |
| 8.11 | `components/ProxyStatus.tsx:353` | toast | Copied the environment. Paste it into a terminal to proxy what runs there. | Copied the environment. Paste it into a terminal. | | The instruction stays; what it does is on the button's own row. |
| 8.12 | `components/FlowList.tsx:1372`, `components/Sidebar.tsx:206` | toast | Copied value | Copied to clipboard | | Yaak's one copy toast. |
| 8.13 | `lib/flowActions.ts:138` | toast | Copied cURL / Copied URL / Copied request body / Copied response body | Copied to clipboard | | |
| 8.14 | `components/ProxyStatus.tsx:356`, `components/FlowList.tsx:1374`, `components/Sidebar.tsx:208`, `lib/flowActions.ts:140` | error toast | Failed to copy: {error} / Failed to copy value: {error} / Failed to copy {thing}: {error} | Failed to copy: {error} | | One wording; the reason stays. |
| 8.15 | `lib/flowActions.ts:165`, `:169` | error toast | Failed to copy cURL: {error} | keep | | |
| 8.16 | `lib/flowActions.ts:134` | info toast | There is no {thing} to copy | keep | | |
| 8.17 | `lib/flowActions.ts:176` | info toast | Body is binary or truncated; export HAR instead | The body is binary or truncated. Export as HAR instead. | | No semicolons. |
| 8.18 | `lib/flowActions.ts:40` | toast | Saved {size} | keep | | |
| 8.19 | `lib/flowActions.ts:36` | toast, appended | as it was on the wire, since it failed to decode | as received, since it failed to decode | | |
| 8.20 | `lib/flowActions.ts:37` | toast, appended | cut at the capture limit | truncated at the capture limit | | |
| 8.21 | `lib/flowActions.ts:43` | error toast | Failed to save the {request or response} body: {error} | keep | | |
| 8.22 | `lib/flowActions.ts:75` | toast | Exported 1 flow as HAR / Exported {n} flows as HAR | keep | | |
| 8.23 | `lib/flowActions.ts:217`, `components/SaveDialog.tsx:254` | toast | Exported the flow as HAR | Exported 1 flow as HAR | | The same event as 8.22. |
| 8.24 | `lib/flowActions.ts:79`, `:219`, `components/SaveDialog.tsx:257` | error toast | Failed to export HAR: {error} | keep | | |
| 8.25 | `components/SaveDialog.tsx:105-106` | toast | Saved {name} to {workspace}, in {folder}. Yaak picks it up within a second. | Saved {name} to {workspace}, in {folder} | | How fast Yaak shows it is an internal. See flag D3. |
| 8.26 | `components/SettingsProxy.tsx:283` | toast | Captured data reset | Recorded data reset | | |
| 8.27 | `lib/opener.ts:11` | error toast | Failed to show {path}: {error} | Failed to reveal {path}: {error} | | |
| 8.28 | `lib/opener.ts:20` | error toast | Failed to open {url}: {error} | keep | | |
### 9. Dialogs
| # | Where | What | Current | Proposed | Tooltip | Note |
| --- | --- | --- | --- | --- | --- | --- |
| 9.1 | `components/ClearFlowsButton.tsx:69` | dialog title | Clear traffic? | Clear Flows | | Titles are never questions. Matches the button and the shortcut's name; "Clear Traffic" is the alternative. |
| 9.2 | `components/ClearFlowsButton.tsx:106-107` | dialog body | 1 flow will be removed. / {n} flows will be removed. | Permanently clear 1 flow? / Permanently clear {n} flows? | | Yaak's confirm pattern, "Permanently delete `x`?", with the button's verb. |
| 9.3 | `components/ClearFlowsButton.tsx:109` | checkbox | Don't ask again | keep (user's wording) | | |
| 9.4 | `components/ClearFlowsButton.tsx:113-114` | button | Cancel / Clear | keep | | |
| 9.5 | `components/SavedSearchDialog.tsx:50` | dialog title | Save search / Rename search | Save Search / Rename Search | | |
| 9.6 | `components/SavedSearchDialog.tsx:61` | input label | Name | keep | | |
| 9.7 | `components/SavedSearchDialog.tsx:70-72` | button | Cancel / Save / Rename | keep | | |
| 9.8 | `proxy-lib/saved_queries.rs:64`, `:74` | error toast | A saved search needs a name / Nothing to save | keep | | |
| 9.9 | `components/AddColumnDialog.tsx:92` | dialog title | Add Column | keep | | |
| 9.10 | `components/AddColumnDialog.tsx:126-127` | button | Cancel / Add | keep | | |
| 9.11 | `components/SaveDialog.tsx:43` | dialog title | Save as Yaak request | Save as Yaak Request | | |
| 9.12 | `components/SaveDialog.tsx:44` | dialog description | A new request in Yaak with this flow's method, URL, headers and body. | A new request will be created in Yaak. | | The form below shows the method, URL, headers and body. |
| 9.13 | `components/SaveDialog.tsx:118` | banner | Yaak has no workspaces yet. Create one in Yaak, then save here. | Create a workspace in Yaak first. | | |
| 9.14 | `components/SaveDialog.tsx:145-173` | form labels | Workspace / Folder / No folder / Name | keep | | |
| 9.15 | `components/SaveDialog.tsx:178-179` | heading | 1 header left out / {n} headers left out | keep | | |
| 9.16 | `components/SaveDialog.tsx:322` | heading, after | Yaak sets these itself when it sends | Yaak sets these itself when it sends the request. | | |
| 9.17 | `components/SaveDialog.tsx:324` | heading, after | Yaak sets these itself, or they only meant something to the proxy | Yaak sets these, or they were meant for the proxy. | | |
| 9.18 | `components/SaveDialog.tsx:311-315` | tooltip | Yaak sets it when it sends / Only meant for the proxy / The body is saved decoded | Yaak sets it when it sends the request. / It was only meant for the proxy. / The body is saved decoded. | | |
| 9.19 | `components/SaveDialog.tsx:201`, `:234` | row label | Kept / Note | keep | | |
| 9.20 | `components/SaveDialog.tsx:203-204` | row value | {n} headers, and no body / {n} headers, and the body as {type} | keep | | |
| 9.21 | `proxy-lib/lib.rs:2781-2791` | row value, type | JSON / XML / GraphQL / text | keep | | |
| 9.22 | `proxy-lib/lib.rs:2787-2788` | row value, type | form URL-encoded with 1 field / form URL-encoded with {n} fields | a URL-encoded form with 1 field / a URL-encoded form with {n} fields | | Reads after "the body as". |
| 9.23 | `proxy-lib/convert.rs:68` | note | The Authorization header is saved as captured, in plain text. | The Authorization header is saved in plain text. | | The dialog matches on the word "Authorization" to colour it. |
| 9.24 | `proxy-lib/convert.rs:73` | note | The body was cut at the capture limit, so only its first {n} bytes are saved. | Only the first {n} bytes of the body are saved. | | |
| 9.25 | `proxy-lib/convert.rs:173` | note | The body is binary and is saved as text, so bytes that are not UTF-8 are replaced. | The body is binary and may be altered when saved. | | |
| 9.26 | `proxy-lib/convert.rs:199` | note | The multipart body is saved as text with its Content-Type, not as form fields. | The multipart body is saved as text, not form fields. | | |
| 9.27 | `components/SaveDialog.tsx:213` | footer | Yaak picks it up within a second | (remove) | | An internal, and cut from the toast too, 8.25. See flag D3. |
| 9.28 | `components/SaveDialog.tsx:216` | button | Cancel | keep | | |
| 9.29 | `components/SaveDialog.tsx:218` | button | Save request | Save Request | | |
| 9.30 | `components/SaveDialog.tsx:265` | dialog heading | Saving needs Yaak | Yaak Required | | As Yaak's "Credentials Required". |
| 9.31 | `components/SaveDialog.tsx:267-269` | dialog body | This flow becomes a request in one of Yaak's workspaces, and Yaak has not been opened on this Mac. Yaak is free for personal use and opens the request with the same viewers you see here. | This flow will be saved as a request in one of Yaak's workspaces, but Yaak has not been opened on this computer. | | The first sentence only, as you asked; nothing promoting Yaak. Shorter still: "Yaak has not been opened on this computer." "This Mac" showed on every platform; see flag F2. |
| 9.32 | `components/SaveDialog.tsx:246` | footer | Or keep the flow as a file | The flow can be exported as a file instead. | | |
| 9.33 | `components/SaveDialog.tsx:249` | button | Export HAR | Export as HAR | | |
| 9.34 | `components/SaveDialog.tsx:261` | button | Download Yaak | keep | | |
| 9.35 | `components/HelperDialog.tsx:51` | dialog title | Turn on Yaak Proxy under Allow in the Background | Approve Helper | | The instruction moves to the description. |
| 9.36 | `components/HelperDialog.tsx:52` | dialog description | In System Settings > General > Login Items. Newer macOS calls the list Background App Activity. | Turn on Yaak Proxy in System Settings > General > Login Items. | | See flag F7. |
| 9.37 | `components/HelperDialog.tsx:80` | status | Waiting for approval | keep | | A progress label. |
| 9.38 | `components/HelperDialog.tsx:85-87` | button | Cancel / Close / Open System Settings | keep | | |
### 10. Settings
The General, Theme and Interface tabs are Yaak's own shared pages and are not reviewed here.
| # | Where | What | Current | Proposed | Tooltip | Note |
| --- | --- | --- | --- | --- | --- | --- |
| 10.1 | `components/Settings.tsx:156-181` | tab | General / Theme / Interface / Proxy / License | keep | | |
| 10.2 | `components/Settings.tsx:182` | empty state | Coming soon | keep | | Better to hide the tab until it has content; that is a behaviour change. |
| 10.3 | `components/Settings.tsx:226-230` | button | Reveal in Finder / Show in Explorer / Show in Folder | Reveal in Finder / Show in Explorer / Show in File Manager | | |
| 10.4 | `components/SettingsProxy.tsx:61` | page title | Proxy | keep | | |
| 10.5 | `components/SettingsProxy.tsx:61` | page description | Listening, decryption, traffic and terminals | Configure the port, decryption, recorded traffic, and terminal environment. | | An imperative sentence, as Yaak's page descriptions are. |
| **Proxy** | | | | | | |
| 10.6 | `components/SettingsProxy.tsx:63` | section heading | Proxy | keep | | |
| 10.7 | `components/SettingsProxy.tsx:150` | row title | Port | keep | | |
| 10.8 | `components/SettingsProxy.tsx:150` | row description | Restarts the proxy | Changing the port restarts the proxy. | | |
| 10.9 | `components/SettingsProxy.tsx:67` | row title | HTTP version | keep | | |
| 10.10 | `components/SettingsProxy.tsx:68` | row description | For new connections | The change applies to new connections. | | |
| 10.11 | `components/SettingsProxy.tsx:73-74` | option | Auto / HTTP/1.1 only | Automatic / HTTP/1.1 only | | As Yaak's own HTTP version select. |
| 10.12 | `components/SettingsProxy.tsx:80-81` | row title and description | Override system proxy / At launch | Override system proxy at launch / Traffic from every app will be recorded. | | "At launch" is the end of the title, not a description. |
| 10.13 | `components/SettingsProxy.tsx:182` | row title | Helper | keep | | |
| 10.14 | `lib/systemProxy.ts:79-80` | row description | Not installed. Changes use the password prompt. | The helper is not installed. | The helper changes the system proxy without a password prompt. | One tooltip for the Helper row, the same in every state but 10.17. |
| 10.15 | `lib/systemProxy.ts:85-86` | row description | Awaiting approval. Changes use the password prompt. | The helper is awaiting approval. | The helper changes the system proxy without a password prompt. | |
| 10.16 | `lib/systemProxy.ts:91-94` | row description | Enabled. No password prompts. | The helper is installed. | The helper changes the system proxy without a password prompt. | |
| 10.17 | `lib/systemProxy.ts:95` | row description | Enabled. One more password prompt puts back your settings. | The helper is installed. | One more password prompt is needed to restore the system proxy settings. | Shown while the system proxy is still set the old way. |
| 10.18 | `lib/systemProxy.ts:66-67` | row description | Not in dev builds. Changes use the password prompt. | The helper is not available in dev builds. | The helper changes the system proxy without a password prompt. | |
| 10.19 | `lib/systemProxy.ts:73-74` | row description | Not found by macOS. Changes use the password prompt. | macOS could not find the helper. | The helper changes the system proxy without a password prompt. | |
| 10.20 | `components/SettingsProxy.tsx:200-203` | button | Install / Approve / Unregister | Install / Approve / Uninstall | | The pair of Install. |
| **Decryption** | | | | | | |
| 10.21 | `components/SettingsProxy.tsx:91` | section heading | Decryption | keep | | |
| 10.22 | `components/DecryptListEditor.tsx:130`, `:135` | row title and description | Certificate / Yaak Proxy CA | keep | | Its literal name in the keychain. |
| 10.23 | `components/DecryptListEditor.tsx:140-147` | status and button | Trusted / Not trusted / Trust / Waiting for macOS | keep | | |
| 10.24 | `components/DecryptListEditor.tsx:159` | button | Reveal | Reveal in Finder / Show in Explorer / Show in File Manager | | The app's other reveal buttons name the place. |
| 10.25 | `components/DecryptListEditor.tsx:77` | row title | Decrypt everything | keep | | |
| 10.26 | `components/DecryptListEditor.tsx:78` | row description | Except built-in exclusions | Excluded hosts and apps stay encrypted. | | The same sentence as 2.30 and 8.2. |
| 10.27 | `components/DecryptListEditor.tsx:91` | heading | Hosts and apps | keep | | |
| 10.28 | `components/DecryptListEditor.tsx:92` | hint | New connections | Changes apply to new connections. | | Takes over from the tooltip, 10.29. |
| 10.29 | `components/DecryptListEditor.tsx:302` | tooltip | Applies to new connections | (remove) | | Said twice. See flag D4. |
| 10.30 | `components/DecryptListEditor.tsx:310` | placeholder | Add host or app | keep | | |
| 10.31 | `lib/decrypt.ts:474-476` | completion info | Exclude / Host or *.site | Exclude / Host or *.domain | | |
| 10.32 | `lib/decrypt.ts:477` | completion info | App name or bundle id | App name or bundle ID | | |
| 10.33 | `components/DecryptListEditor.tsx:289` | validation | Not a host or app | Invalid host or app | | As "Invalid value" and "Invalid name". |
| 10.34 | `components/DecryptListEditor.tsx:109` | empty state | Nothing listed | No hosts or apps | | |
| 10.35 | `lib/decrypt.ts:103-104` | list row | Everything / Nothing | keep | | |
| 10.36 | `components/DecryptListEditor.tsx:190`, `:179` | list row | {n} built-in exclusions / Apple, pinned apps | keep | | |
| 10.37 | `components/DecryptListEditor.tsx:223`, `:235` | tooltip and badge | Excluded / Decrypted / Apple / Pinned | keep | | |
| 10.38 | `components/DecryptListEditor.tsx:242` | tooltip | Remove {entry} | keep | | |
| **Traffic** | | | | | | |
| 10.39 | `components/SettingsProxy.tsx:95` | section heading | Traffic | keep | | |
| 10.40 | `components/SettingsProxy.tsx:220` | row title | Captured traffic | Recorded traffic | | |
| 10.41 | `components/SettingsProxy.tsx:226-227` | option | Clear on launch / Keep between launches | keep | | |
| 10.42 | `components/SettingsProxy.tsx:243` | row title | Size limit | keep | | |
| 10.43 | `components/SettingsProxy.tsx:243` | row description | Oldest flows dropped past it | The oldest flows will be cleared first. | | |
| 10.44 | `components/SettingsProxy.tsx:247`, `lib/settings.ts:141` | value and option | {size} used / Unlimited | keep | | |
| 10.45 | `components/SettingsProxy.tsx:270` | row title | Confirm before clearing | keep | | |
| 10.46 | `components/SettingsProxy.tsx:288` | row title | Reset captured data | Reset recorded data | | |
| 10.47 | `components/SettingsProxy.tsx:288` | row description | Deletes all flows and their keys | All traffic and its encryption keys will be deleted. | | |
| 10.48 | `components/SettingsProxy.tsx:290` | button | Reset… | Reset | | |
| 10.49 | `components/SettingsProxy.tsx:296` | dialog title | Reset captured data? | Reset Recorded Data | | Titles are never questions. |
| 10.50 | `components/SettingsProxy.tsx:297` | dialog body | Every captured flow and the keys that encrypt them are deleted. | Permanently delete all traffic and its encryption keys? | | Yaak's confirm pattern. |
| 10.51 | `components/SettingsProxy.tsx:302-304` | button | Cancel / Reset | keep | | |
| **Terminal** | | | | | | |
| 10.52 | `components/SettingsProxy.tsx:102` | section heading | Terminal | keep | | |
| 10.53 | `components/SettingsProxy.tsx:333` | row title | Environment | keep | | |
| 10.54 | `components/SettingsProxy.tsx:366` | row description | Proxies one terminal instead of the system | Traffic from one terminal will be recorded. | | |
| 10.55 | `components/SettingsProxy.tsx:342`, `lib/env.ts:6-9` | tooltip and options | Shell / bash / zsh / fish / PowerShell / cmd | keep | | |
| 10.56 | `components/SettingsProxy.tsx:363` | button | Open Terminal | keep | | |
| 10.57 | `components/SettingsProxy.tsx:354` | tooltip | A new Terminal window with these set | A new Terminal window will open with these variables set. | | On a text button, so it explains. |
| 10.58 | `components/SettingsProxy.tsx:355` | tooltip, disabled | Not available on this platform yet | Opening a terminal is only supported on macOS. | | As the error, 12.30. |
### 11. App menu
| # | Where | What | Current | Proposed | Tooltip | Note |
| --- | --- | --- | --- | --- | --- | --- |
| 11.1 | `tauri/window_menu.rs:59-161` | menu titles | File / Edit / View / Go / Window / Help | keep | | |
| 11.2 | `tauri/window_menu.rs:109` | menu item | Quit Yaak Proxy | keep | | |
| 11.3 | `tauri/window_menu.rs:127` | menu item | Export All Flows as HAR | keep | | |
| 11.4 | `tauri/window_menu.rs:164`, `:168` | menu item | Back / Forward | keep | | |
| 11.5 | `tauri/lib.rs:139`, `:235` | window title | Yaak Proxy Settings / Yaak Proxy | keep | | |
| 11.6 | `proxy-lib/actions.rs:38`, `:46` | action label | Pause or Resume Recording / Clear Flows | keep | | Not shown anywhere today. |
### 12. Errors and the locked state
| # | Where | What | Current | Proposed | Tooltip | Note |
| --- | --- | --- | --- | --- | --- | --- |
| **Locked state** | | | | | | |
| 12.1 | `components/LockedState.tsx:31` | heading | Encrypting stored traffic, {percent} | Encrypting recorded traffic, {percent} | | |
| 12.2 | `components/LockedState.tsx:43` | description | Earlier traffic shows here once it's encrypted. | keep | | |
| 12.3 | `components/LockedState.tsx:38` | heading | Traffic is locked | keep | | |
| 12.4 | `lib/locked.ts:24` | description | Allow Yaak Proxy to use its key in Keychain Access, then retry. | keep | | Names Credential Manager or the keyring off macOS. |
| 12.5 | `components/LockedState.tsx:54` | button | Retry | keep | | |
| 12.6 | `proxy-lib/lib.rs:2062` | error prefix | Traffic is locked: {reason} | keep | | |
| 12.7 | `proxy-lib/lib.rs:236` | error reason | traffic captured before this version is being encrypted. | traffic recorded before this version is being encrypted. | | |
| 12.8 | `proxy-lib/lib.rs:239` | error reason | its key has not been read yet. | keep | | |
| 12.9 | `proxy-lib/lib.rs:278` | error reason | {12.12}, so the traffic it stored can't be encrypted yet. Take over or quit it, then retry. | {12.12}. Take over or quit it, then retry. | | Why it blocks is an internal; what to do stays. |
| 12.10 | `proxy-lib/db.rs:158` | error reason | The proxy's captured traffic is encrypted, but its key is missing from {folder} | The encryption key is missing from {folder}. | | |
| 12.11 | `proxy-lib/db.rs:328-332` | error reason | Yaak Proxy stores traffic encrypted and couldn't read its key from the keychain: {reason}. Allow it access, then retry. | Failed to read the encryption key from the keychain: {reason} | | The remedy is the line above it on screen, 12.4. Code matches on its opening words, one constant, so safe. |
| **Starting and taking over** | | | | | | |
| 12.12 | `proxy-lib/control.rs:392` | error | The proxy is already running in {owner} (pid {pid}) | keep | | |
| 12.13 | `proxy-lib/control.rs:397-398` | error, owner | Yaak Proxy / the yaak CLI | Yaak Proxy / the CLI | | |
| 12.14 | `engine/lib.rs:569` | error | Failed to bind port {port}: {error} | keep | | |
| 12.15 | `proxy-lib/lib.rs:723` | error | The proxy in {owner} (pid {pid}) stopped but did not let go of it | Failed to take over from {owner} (pid {pid}) | | Yaak's error form. "It" had nothing to point at. |
| 12.16 | `proxy-lib/lib.rs:949` | error | {12.12}. Quit it first. | keep | | |
| 12.17 | `proxy-lib/lib.rs:855` | error toast | The port must be from 1 to 65535 | keep | | |
| **System proxy and helper** | | | | | | |
| 12.18 | `proxy-lib/macos.rs:282` | macOS password prompt | Yaak Proxy wants to change your system proxy settings. | keep | | macOS's own form for this prompt. |
| 12.19 | `proxy-lib/macos.rs:290` | error | Cancelled in the password dialog | The password prompt was cancelled. | | The same sentence as 2.25. See flag D2. |
| 12.20 | `proxy-lib/lib.rs:811` | error | The proxy is not running, so there is nothing to route to | The proxy is not running | | |
| 12.21 | `proxy-lib/helper.rs:136-137` | error | Failed to install the helper: {error} | keep | | |
| 12.22 | `proxy-lib/helper.rs:156` | error | Failed to remove the helper: {error} | Failed to uninstall the helper: {error} | | Matches the button, 10.20. |
| 12.23 | `proxy-lib/helper.rs:161` | error | The helper is only available in the installed app, not a dev build | The helper is not available in dev builds | | As 10.18. A one-sentence error takes no period, as the kept ones. |
| 12.24 | `proxy-lib/helper.rs:173` | error | The helper is not running: {error} | keep | | |
| 12.25 | `proxy-lib/helper.rs:184` | error | The helper hung up: {error} | The helper disconnected: {error} | | |
| 12.26 | `proxy-lib/helper.rs:188` | error | The helper did not answer: {error} | The helper did not respond: {error} | | |
| 12.27 | `proxy-lib/helper.rs:190` | error | The helper's answer was not understood: {error} | The helper sent an invalid response: {error} | | |
| 12.28 | `proxy-lib/ca.rs:106`, `proxy-lib/macos.rs:504` | error | Trusting the certificate from the app is only supported on macOS / Setting the system proxy is only supported on macOS | keep | | |
| 12.29 | `proxy-lib/helper.rs:217`, `:222` | error | The helper is only available on macOS | The helper is only supported on macOS | | One form for all four. |
| 12.30 | `proxy-lib/terminal.rs:47` | error | Opening a terminal with the proxy set is only supported on macOS so far | Opening a terminal is only supported on macOS | | |
| 12.31 | `proxy-lib/ca.rs:73`, `:92`, `proxy-lib/macos.rs:287`, `:300`, `proxy-lib/terminal.rs:36` | error | security exited with {status} / Failed to run {command}: {error} | keep | | |
| **Flows and bodies** | | | | | | |
| 12.32 | `proxy-lib/lib.rs:3236` | flow error | Rejected the proxy's certificate | Certificate rejected by the app | | See flag D9. |
| 12.33 | `proxy-lib/lib.rs:3226`, `:3229` | flow error | Yaak Proxy stopped before the stream ended / Yaak Proxy stopped before the connection closed | keep | | |
| 12.34 | `proxy-lib/lib.rs:3232-3233` | error | Not a request: the client's TLS handshake failed before it sent one | The TLS handshake failed before any request was sent | | |
| 12.35 | `proxy-lib/bodies.rs:115` | banner | Failed to decode {encoding} body: {error} | keep | | |
| 12.36 | `engine/decode.rs:104` | banner, reason | No output from the deflate body within the capture limit | keep | | |
| 12.37 | `engine/decode.rs:135`, `:203` | banner, reason | The gzip header is too long / Not a gzip stream | keep | | |
| 12.38 | `proxy-lib/bodies.rs:16` | error toast | A WebSocket has no body after its 101 response. Its frames are its messages. | A WebSocket has no body | | Reaches the UI when saving such a body to a file. |
| 12.39 | `proxy-lib/flows.rs:701` | error toast | Failed to clear flows: {error} | keep | | |
| 12.40 | `proxy-lib/flows.rs:722` | error | Failed to delete captured traffic: {error} | Failed to delete recorded traffic: {error} | | |
| 12.41 | `proxy-lib/filter_history.rs:21` | error toast | Failed to {read, record, clear} the filter history: {error} | Failed to {read, record, clear} the search history: {error} | | |
| 12.42 | `proxy-lib/lib.rs:2832-2887` | error toast | Failed to create {path}: {error} / Failed to write {path}: {error} | keep | | |
| **Saving to Yaak, and settings** | | | | | | |
| 12.43 | `proxy-lib/save.rs:73` | banner | Yaak's database was not found at {path}. Install Yaak and open it once. | keep | | |
| 12.44 | `proxy-lib/save.rs:79`, `:84`, `:144` | banner | Failed to open Yaak's database: {error} / Failed to save the request to Yaak: {error} | keep | | |
| 12.45 | `proxy-lib/save.rs:119-125` | banner | No workspace {id} in Yaak / No folder {id} in Yaak / Folder {id} is not in workspace {id} | keep | | |
| 12.46 | `proxy-lib/lib.rs:1489` | banner | Failed to record the saved request on the flow: {error} | keep | | |
| 12.47 | `proxy-lib/settings.rs:232-244` | error toast | Invalid settings in {path}: {error} / Failed to read {path}: {error} / Failed to write {path}: {error} | keep | | |
| 12.48 | `tauri/lib.rs:134` | error toast | The main window is not open to open Settings over | Failed to open Settings: the main window is closed | | |
Left out as developer-only or never reaching the window: the control API's and the CLI client's errors (`proxy-lib/control.rs`, `proxy-lib/client.rs`), gRPC schema attachment (`proxy-lib/grpc.rs:101-305`), app icon failures (`proxy-lib/attribution.rs`), "No flow {id}" lookups, decrypt-entry parse errors (`engine/decrypt.rs`, the input validates first), and the text the proxy sends back to the proxied app (`engine/request.rs`).
## Terminology decisions
One word per thing, applied through the table.
| Thing | The app says | Never |
| --- | --- | --- |
| The stream as a whole | **traffic** ("All Traffic", "Traffic is locked", "Recorded traffic", "Traffic from every app will be recorded.") | "captured data"; "flows" where the whole stream is meant |
| One row of it | **flow**, counted as **flows** ("4,102 flows", "1 new flow", "Search 4,102 flows", "Export All Flows as HAR", "Clear Flows") | "request" for a row. "Requests" stays in your encrypted-card line and for Yaak requests ("Save as Yaak Request"). |
| Keeping traffic | **Recording** and **Paused**; verbs "Pause Recording", "Resume Recording"; traffic is "recorded" | "stored", "captured", "kept". "The capture limit" stays as that limit's name (see flag N17). |
| Reading HTTPS | verbs **Decrypt** and **Stop Decrypting**; the feature is **decryption**; states **Encrypted** and **Decrypted** | "passed through", "not on the decrypt list", "tunnelled" |
| A `!` entry | **Exclude**, **Stop Excluding**, **Excluded**; what they name are **excluded hosts and apps** | "Apple services and apps that pin" and "built-in exclusions" as two names for one clause |
| Emptying | **Clear** empties the list of flows or a history; **Remove** takes one item off a list; **Delete** destroys a saved search or all data; **Reset** goes back to a default | "dropped", "removed" for flows; "Clear Sorting" beside "Reset Sort" |
| What sent a flow | **app** | "program", "client", "process". "Client" and "Server" stay only as the two HTTP sides in the Info tab. |
| `*.example.com` and an exact name | **domain** and **host** | "site" |
| The header box and what is saved from it | **search** ("Recent searches", "Save Search") | "filter" for the same thing. "Filter" stays in your three menu items and in the body viewer's JSONPath filter. |
| A body over the capture limit | **truncated** | "cut", "kept" |
| A body as it arrived | **as received** | "as it was on the wire" |
| The CA | **certificate**; "Yaak Proxy CA" only as its literal keychain name | "the proxy's certificate", "proxy CA" |
| The macOS proxy setting | **Override system proxy**; in sentences, "the system proxy is overridden" | "routing", "turn on the system proxy" |
| Settings | **Settings** is the app's; **System Settings** is macOS's, always in full | "Settings" for the macOS pane |
| A handshake the app turned down | **rejected** | "refused" |
| Trying once more | **Retry** | "Try Again" |
| A macOS password prompt | **the password prompt**; in flight, the label **Waiting for macOS** | "password dialog", "Waiting for your password" |
| Another running Yaak Proxy | **another copy of Yaak Proxy** | "another Yaak Proxy window", "the engine" |
| The other owner | **the CLI** | "the yaak CLI" |
| Showing a file | **Reveal in Finder**, **Show in Explorer**, **Show in File Manager** (Yaak's three) | "Show in Folder" |
## Flags
### Wrong or misleading as facts
- **F1.** `components/FirstRun.tsx:52` says "Two steps, once." Only the first step is the user's; the second is a wait for traffic.
- **F2.** Two strings name macOS on every platform. `components/FirstRun.tsx:142` says "macOS may ask" with no platform check, while `lib/locked.ts` already names Credential Manager and the keyring. `components/SaveDialog.tsx:268` says "on this Mac" in a dialog Windows and Linux also show.
- **F3.** `components/ProxyStatus.tsx:160` says the password is asked "until the helper is approved in Settings". It also shows when the helper is not installed at all, where there is nothing to approve, and "Settings" reads as the app's while the approval is given in System Settings.
- **F4.** "Except Apple services and apps that pin" is fixed text in eight places. It is false once the built-in exclusions are removed (as in your dev app), and it leaves out the user's own exclusions, which apply too. "Excluded hosts and apps stay encrypted." is always true.
- **F5.** `lib/encryptedCard.ts:274` shows "Not on the decrypt list" in exactly the case where the list now decrypts the connection; the line under it then says it opened before that.
- **F6.** Keys are fixed to macOS: `components/FlowDetail.tsx:32-36` shows ⌘R and ⌘⇧K where Windows and Linux bind Ctrl+Shift+R and Ctrl+Shift+K (`proxy-lib/actions.rs:40,48`), and `components/SearchBar.tsx:201` shows ⌘F.
- **F7.** `components/HelperDialog.tsx:52` says "Newer macOS calls the list Background App Activity" beside a title that calls it "Allow in the Background". I could not check which name the current macOS uses; the proposal keeps the title's.
- **F8.** `components/TakeOverBanner.tsx:32` and `components/FirstRun.tsx:65` say "Another Yaak Proxy window". The other owner is another running copy of the app, not a window of this one.
- **F9.** `components/FlowInfo.tsx:82` labels a request ID "Saved As", which reads as a name. "Yaak Request ID" is exact.
- **F10.** `components/FlowListEmpty.tsx:37` reads "Recording on port --" when the port is not known. The proposed sentence would read "Traffic sent to port -- will be recorded.", so the case needs its own line or no port.
- **F11.** The search field `@time` means when a flow started (the Started column), while the Time column is `@duration`. Prose cannot fix that; it needs an alias or a rename.
- **F12.** `lib/encryptedCard.ts:424` says "Passed through until relaunch" without saying whose. The pass-through list lives in the proxy and is emptied when Yaak Proxy restarts, so the proposal names Yaak Proxy.
### Duplicated messages for one event
- **D1.** The take-over strip says where the proxy runs twice: "Running in another Yaak Proxy" then "Another Yaak Proxy window owns the engine" (`components/TakeOverBanner.tsx:20,32`).
- **D2.** A cancelled password prompt is worded in Rust ("Cancelled in the password dialog", `proxy-lib/macos.rs:290`) and reworded in the window by matching on "cancel" ("Cancelled in the password prompt", `lib/systemProxy.ts:40`). Both become "The password prompt was cancelled."
- **D3.** "Yaak picks it up within a second" is in the save dialog's footer and again in its toast (`components/SaveDialog.tsx:106,213`). The proposals drop both.
- **D4.** "New connections" as a hint and "Applies to new connections" as a tooltip sit on the same input (`components/DecryptListEditor.tsx:92,302`).
- **D5.** "Not running" has three wordings: the pill and row title, "The proxy has not started." under that title (`components/ProxyStatus.tsx:90-93`), and "The proxy is not running." in the list (`components/FlowListEmpty.tsx:34`). The proposals leave two: the state name, and "The proxy failed to start." for the reason.
- **D6.** Encrypting older traffic is explained four ways (`components/FirstRun.tsx:94`, `components/ProxyStatus.tsx:69`, `components/LockedState.tsx:43`, `proxy-lib/lib.rs:236`).
- **D7.** Paused is explained four ways (`components/ProxyStatus.tsx:84`, `components/FirstRun.tsx:98`, `components/FlowListEmpty.tsx:36` and `:59`). The proposals leave one sentence, "Traffic passes through without being recorded.", in the status menu and the empty list.
- **D8.** A truncated body is said eight ways: "Body cut at the capture limit", "Body cut", "First {size} kept", "Showing the first {size} of {size}, the capture limit", "First {size} of {size} kept", "cut at the capture limit", "The message is cut at the capture limit.", "The body was cut at the capture limit" (rows 4.19, 6.6, 6.7, 6.25, 6.27, 8.20, 6.44, 9.24).
- **D9.** "Rejected the proxy's certificate" is built once in Rust as the flow's error and again in the list's tooltip (`proxy-lib/lib.rs:3236`, `components/FlowList.tsx:932`).
- **D10.** Exporting one flow says "Exported the flow as HAR" from the flow's menu and "Exported 1 flow as HAR" from the list's (`lib/flowActions.ts:217,75`).
- **D11.** Copying has five success toasts and four failure toasts (rows 8.11 to 8.14) where Yaak has one of each.
- **D12.** A macOS password prompt in flight reads "Waiting for macOS" for the certificate and "Waiting for your password" for the system proxy (`components/ProxyStatus.tsx:247`, `lib/systemProxy.ts:31`).
- **D13.** "Certificate not trusted" is both a state (pill, strip, warning toast) and the lead of an error toast when trusting fails (`hooks/useDecrypt.tsx:49,123`).
- **D14.** The same clause about exclusions is written two ways within one feature: "Except Apple services and apps that pin" in the status menu and toasts, "Except built-in exclusions" in Settings (`components/DecryptListEditor.tsx:78`).
### One thing with two names
- **N1.** Recorded traffic is "captured traffic" (Settings, first run), "stored traffic" (the encrypting titles), "captured data" (the reset row) and "the capture database" (the take-over strip).
- **N2.** search and filter for the header box: "Search flows", "Save search", "Clear search" beside "Recent filters", "No recent filters" and Rust's "filter history".
- **N3.** Time (column), Total (Info tab), Total time (search help). Latency (column), Headers (Info tab).
- **N4.** domain and site: the sidebar's "Domains" beside the Choose menu's "Sites", "Decrypt 3 Sites" and "Host or *.site".
- **N5.** app, program, client: "App or program", "A client rejected the certificate", "Cancelled by the client" beside "Cancelled by the app".
- **N6.** "Clear Sorting" in a column's menu and "Reset Sort" in the columns menu, for the same action.
- **N7.** "Override system proxy", "Turn on the system proxy", "System proxy on", "Routing".
- **N8.** "built-in exclusions", "Apple services and apps that pin", "Excluded by default".
- **N9.** "the CLI" and "the yaak CLI".
- **N10.** "Retry" and "Try Again".
- **N11.** rejected and refused ("macOS may refuse it").
- **N12.** The helper is Installed by its button, Unregistered by the opposite button, "removed" in that button's error, and "Enabled" as a state.
- **N13.** "password prompt" and "password dialog".
- **N14.** "Reveal", "Reveal in Finder", and "Failed to show" for the same action; "Show in Folder" where Yaak says "Show in File Manager".
- **N15.** "Working folder" (search help), "Directory" (Info tab).
- **N16.** inflate, decompress and decode for a compressed message.
- **N17.** "the capture limit" names a per-body limit Settings never shows, while Settings has a "Size limit" that is a different limit. The proposals keep the name; showing the limit in Settings would settle it.
- **N18.** "Export HAR" and "Export as HAR".
- **N19.** "Certificate", "Yaak Proxy CA", "the proxy's certificate".
- **N20.** Passed through, not on the decrypt list, encrypted: three ways to say a connection was not decrypted.
- **N21.** Clearing is "Clear flows" on its button and in the shortcut list, and "Clear traffic?" in its dialog. The proposal takes "Clear Flows", the commoner; "Clear Traffic" would fit the whole-stream sense as well.
- **N22.** stored, captured, kept and recorded for the same act: "Every flow is stored", "are captured", "First {size} kept", "Recording".
## Voice guide, from the Yaak client
Client paths are under `apps/yaak-client/`. `ui/` means `packages/ui/src/components/`.
1. **Two registers.** A label names a thing or an action and stays short. An explanation says what will happen and is a complete sentence with a subject and a period. Labels are buttons, menu items, headings, tabs, select options, icon-button tooltips and state names; explanations are descriptions, status lines, dialog bodies, hints and a toast's second line. "Reset Changes" over "This will discard all uncommitted changes. This cannot be undone." (`components/git/GitDropdown.tsx:228-229`), "Generate Example" over "The current message will be replaced with an example." (`components/GrpcEditor.tsx:150-151`).
2. **Explanations have a subject, and the subject is the user's data.** Passive and "will be" are the usual form. "The current message will be replaced with an example." (`components/GrpcEditor.tsx:151`), "Sharable environments are included in data export and directory sync." (`components/CreateEnvironmentDialog.tsx:51`), "Cookies will appear when a response includes a Set-Cookie header." (`components/CookieDialog.tsx:174`), "Any previously encrypted values will fail to decrypt and will need to be re-entered manually." (`components/WorkspaceEncryptionSetting.tsx:221-222`). The proxy's model: "Traffic from every app will be recorded."
3. **Never a sentence with its subject cut off.** No description in the client opens on a bare third-person verb ("Wraps…", "Sends…", "Records…"). A setting description is an imperative sentence, "Wrap long lines in request and response editors." (`ui/settings/SettingsInterface.tsx:144`), "Choose whether updates are installed automatically or manually." (`ui/settings/SettingsGeneral.tsx:130`), "Use the operating system's standard title bar and window controls." (`ui/settings/SettingsInterface.tsx:197`), or, a minority, a noun phrase, "Font used in request and response editors." (`ui/settings/SettingsInterface.tsx:94`). The proxy's settings act on traffic rather than on the interface, so they take the subject form of rule 2. Articles are never dropped from a sentence.
4. **Instructions are imperative sentences.** "You have uncommitted changes. Commit or reset your changes before pulling." (`components/git/uncommitted.tsx:8`), "Click the menu button to add, remove, or reset keyboard shortcuts." (`components/Settings/SettingsHotkeys.tsx:99`).
5. **A visible explanation is one sentence of about ten words, and the rest goes in an info tooltip or is cut.** Yaak's setting descriptions run six to ten words: "Send proxy credentials with proxied requests." (`components/Settings/SettingsProxy.tsx:107`), "Encrypt workspace secrets and sensitive values at rest." (`components/WorkspaceEncryptionSetting.tsx:137`), "Choose how Yaak should discover or use proxy settings." (`components/Settings/SettingsProxy.tsx:38`). Detail sits behind an info icon: "Sharable environments are included in Directory Sync and data export." (`components/EnvironmentSharableTooltip.tsx:5`), "Authentication was inherited from an ancestor" (`hooks/useAuthTab.tsx:68`). A line that only repeats its title is left out: "Interface font" (`ui/settings/SettingsInterface.tsx:60`) and "Automatically download updates" (`ui/settings/SettingsGeneral.tsx:141`) have none. The one long description in the client is an outlier (`components/Settings/SettingsProxy.tsx:31`).
6. **Buttons:** Title Case, one to three words, verb first, no period. "Add Certificate" (`components/Settings/SettingsCertificates.tsx:226`), "Save to File" (`components/responseViewers/BinaryViewer.tsx:32`), "Apply and Restart" (`ui/settings/SettingsInterface.tsx:209`).
7. **Menu items:** Title Case, verb plus noun. "Clear Send History" (`components/WorkspaceActionsDropdown.tsx:126`), "Expand All Folders" (`components/Sidebar.tsx:632`), "Check for Updates" (`components/SettingsDropdown.tsx:69`). Inconsistent: "Keyboard shortcuts" (`components/SettingsDropdown.tsx:34`), "Delete all" (`components/RecentHttpResponsesDropdown.tsx:106`). Title Case is the majority.
8. **Ellipses on actions:** none. Items that open a dialog are bare: "Manage Cookies" (`components/CookieDropdown.tsx:34`), "Import Data" (`components/SettingsDropdown.tsx:53`), "Workspace Settings" (`components/WorkspaceActionsDropdown.tsx:111`). Only the Git menu adds them ("Commit...", `components/git/GitDropdown.tsx:205`), a minority. Where an ellipsis does appear it is three periods, never "…".
9. **Dialog and confirm titles:** Title Case, the action as verb plus noun, never a question. "Rename Cookie Jar" (`components/CookieDropdown.tsx:48`), "Reset Changes" (`components/git/GitDropdown.tsx:228`), "Uninstall Plugin" (`hooks/useUninstallPlugin.tsx:16`). One stray: "Branch not fully merged" (`components/git/GitDropdown.tsx:408`).
10. **Confirms and destructive actions:** the body says what will be lost and the button repeats the title's verb. Two forms, about even: a statement, "This will discard all uncommitted changes. This cannot be undone." with "Reset" (`components/git/GitDropdown.tsx:229-230`), "This will restore the file to the selected commit." with "Restore" (`components/git/FileHistoryDialog.tsx:39-40`); or a one-line question, "Permanently delete `name`?" with "Delete" (`lib/deleteModelWithConfirm.tsx:29`, `lib/confirm.ts:31`), "Permanently uninstall `name`?" (`hooks/useUninstallPlugin.tsx:20`). The question is Yaak's form for deleting, so the proxy's two destructive confirms take it.
11. **Tooltips on icon buttons:** sentence case, imperative verb plus object, no period. "Toggle sidebar" (`components/SidebarActions.tsx:30`), "Save response to file" (`components/HttpResponsePane.tsx:220`), "Show response history" (`components/RecentHttpResponsesDropdown.tsx:126`). Inconsistent: "Send Request" (`components/FolderLayout.tsx:118`), "Main Menu" (`components/SettingsDropdown.tsx:105`). Sentence case is the majority. A tooltip that explains is a sentence: "This environment will be included in Directory Sync and data exports" (`components/EnvironmentEditDialog.tsx:114`).
12. **Toasts:** the first line is a label, past tense or a state, no period, names in inline code. "Copied to clipboard" (`lib/copy.ts:19`), "Already up-to-date" (`components/git/git-util.ts:13`), "Response saved to `path`" (`hooks/useSaveResponse.tsx:36`). With more to say, a bold title sits over one subtle line: "Yaak 2026.1 is available" over "Download and install the update?" (`lib/initGlobalListeners.tsx:252-254`).
13. **Errors:** "Failed to <verb>" inline, or a short title over the raw error, which is appended and never paraphrased. "Failed to copy" (`components/CopyIconButton.tsx:20`), "Export Failed" (`hooks/useExportData.tsx:14`), "Error pushing changes" over `String(err)` (`components/git/GitDropdown.tsx:179-180`). Titles split between "X Failed" and "Error <verb>ing X"; "Failed to <verb>" is the commonest one-line form.
14. **Empty states:** "No <noun>" or one imperative, no period. "No authentication" (`components/HttpAuthenticationEditor.tsx:46`), "No plugins found" (`components/Settings/SettingsPlugins.tsx:317`), "Select a commit to view diff" (`components/git/FileHistoryDialog.tsx:78`). Casing is mixed ("No Content", `components/WebsocketResponsePane.tsx:236`; "No Headers", `ui/http/ResponseHeaders.tsx:26`); sentence case is the majority. An empty state that explains is a full sentence with its period (`components/CookieDialog.tsx:174,177`).
15. **No results:** "No results for `x`" (`components/Sidebar.tsx:703`), and with suggestions "No results, but found matches for `a` or `b`" (`components/Sidebar.tsx:680`).
16. **Settings pages and sections:** a page has a one-word title and one sentence with a period, "Tweak settings related to the user interface." (`ui/settings/SettingsInterface.tsx:54`), "Configure general settings for update behavior and more." (`ui/settings/SettingsGeneral.tsx:29`). Section headings are Title Case nouns: "App Info" (`ui/settings/SettingsGeneral.tsx:48`), "Custom Proxy" (`components/Settings/SettingsProxy.tsx:61`).
17. **Setting row titles:** sentence case, a verb phrase for a switch. "Wrap editor lines" (`ui/settings/SettingsInterface.tsx:143`), "Enable proxy" (`components/Settings/SettingsProxy.tsx:64`), "Hide window controls" (`ui/settings/SettingsInterface.tsx:170`). Inconsistent: Title Case rows "Update Channel", "Data Directory" (`ui/settings/SettingsGeneral.tsx:102,53`). Sentence case is the majority.
18. **Select options:** sentence case. "Always ask", "Open in current window" (`components/Settings/SettingsInterface.tsx:46-47`), "No proxy" (`components/Settings/SettingsProxy.tsx:53`), "Automatic" (`components/ModelSettingsEditor.tsx:339`).
19. **Placeholders:** an example value or a one-word label. "api.example.com" (`components/DnsOverridesEditor.tsx:140`), "Search" (`components/Sidebar.tsx:586`), "Filter cookies" (`components/CookieDialog.tsx:152`). Some end in "..." ("Search plugins...", `components/Settings/SettingsPlugins.tsx:306`); bare is the majority.
20. **Progress and state names are labels:** a gerund or a participle, no period. "Cloning..." (`components/CloneGitRepositoryDialog.tsx:139`), "Restarting..." (`lib/initGlobalListeners.tsx:169`), "Sending Request" (`ui/http/ResponseBodyViewer.tsx:58`).
21. **Verbs:** a small fixed set, each with one meaning. Delete destroys a thing ("Delete", `components/Sidebar.tsx:527`), Remove takes it off a list ("Remove", `components/responseViewers/RecentFiltersDropdown.tsx:43`), Clear empties a history ("Clear Send History"), Reset goes back to a default ("Reset to Default", `components/Settings/SettingsHotkeys.tsx:239`). Also New, Rename, Duplicate, Copy, Save, Import, Export, Show, Hide, Enable, Reveal, Retry ("Retry", `components/GrpcEditor.tsx:189`).
22. **Names and literal values** go in inline code, not quotes. "Switched branch `x`" (`components/git/GitDropdown.tsx:109`), "Enter a new name for `x`" (`components/CookieDropdown.tsx:51`), "No results for `x`" (`components/Sidebar.tsx:703`).
23. **Never:** apologies (none in the client), exclamation marks (one in the whole client, "Thanks for the feedback!", `components/FeedbackToast.tsx:39`), headings that ask a question outside upsell banners ("Using Git for work?", `components/git/GitCommitDialog.tsx:221`). "Please" exists only in eight long banners, mostly licensing (`ui/settings/SettingsLicense.tsx:178`, `components/EnvironmentEditDialog.tsx:82`). Hedging appears twice, both "may impact performance" (`components/ConfirmLargeRequestBody.tsx:35`, `ui/http/ConfirmLargeResponse.tsx:53`).
24. **Punctuation:** a period on every full sentence, none on a label; no semicolons; counts inline ("Clear 3 Connections", `components/RecentWebsocketConnectionsDropdown.tsx:95`); "and", never "&". A few client sentences miss their period (`components/EnvironmentEditDialog.tsx:114`, `components/git/GitDropdown.tsx:609`); with a period is the majority.
25. **Length:** buttons one to three words, menu items one to four, icon tooltips two to four, a toast's first line two to five. A visible explanation is one sentence of about ten words (rule 5); a tooltip may run to two sentences.
@@ -47,27 +47,14 @@ export function ClearFlowsButton() {
<IconButton <IconButton
className="pointer-events-auto" className="pointer-events-auto"
icon="trash" icon="trash"
title={ title={action === "disabled" || hotkey == null ? "Clear flows" : `Clear flows ${hotkey}`}
locked != null
? "Clear flows (traffic is locked)"
: total === 0
? "Clear flows (none yet)"
: hotkey == null
? "Clear flows"
: `Clear flows ${hotkey}`
}
tooltip tooltip
size="sm" size="sm"
iconColor="secondary" iconColor="secondary"
disabled={action === "disabled"} disabled={action === "disabled"}
onClick={start} onClick={start}
/> />
<Dialog <Dialog open={confirming} onClose={() => setConfirming(false)} size="sm" title="Clear Flows">
open={confirming}
onClose={() => setConfirming(false)}
size="sm"
title="Clear traffic?"
>
{confirming && ( {confirming && (
<ConfirmClear <ConfirmClear
total={total} total={total}
@@ -103,8 +90,8 @@ function ConfirmClear({
<form id={formId} className="flex flex-col gap-3" onSubmit={handleSubmit}> <form id={formId} className="flex flex-col gap-3" onSubmit={handleSubmit}>
<p> <p>
{total === 1 {total === 1
? "1 flow will be removed." ? "Permanently clear 1 flow?"
: `${total.toLocaleString()} flows will be removed.`} : `Permanently clear ${total.toLocaleString()} flows?`}
</p> </p>
<Checkbox checked={dontAskAgain} title="Don't ask again" onChange={setDontAskAgain} /> <Checkbox checked={dontAskAgain} title="Don't ask again" onChange={setDontAskAgain} />
<DialogFooter <DialogFooter
@@ -26,7 +26,8 @@ import {
everythingEdit, everythingEdit,
normalizeEntry, normalizeEntry,
} from "../lib/decrypt"; } from "../lib/decrypt";
import { revealPath } from "../lib/opener"; import { revealLabel, revealPath } from "../lib/opener";
import { getOsType } from "../lib/tauri";
import { useShowToast } from "../lib/toast"; import { useShowToast } from "../lib/toast";
/** /**
@@ -75,21 +76,23 @@ export function DecryptionSettings() {
<CertificateRow /> <CertificateRow />
<SettingRowBoolean <SettingRowBoolean
title="Decrypt everything" title="Decrypt everything"
description="Except built-in exclusions" description="Excluded hosts and apps stay encrypted."
checked={everything} checked={everything}
disabled={busy} disabled={busy}
onChange={(on) => onChange={(on) =>
void change( void change(
everythingEdit(on), everythingEdit(on),
on ? "Decrypting everything" : "Stopped decrypting everything", on ? "Decrypting everything" : "Stopped decrypting everything",
on ? "Except Apple services and apps that pin" : undefined, on ? "Excluded hosts and apps stay encrypted." : undefined,
) )
} }
/> />
<div className="py-4 grid gap-2"> <div className="py-4 grid gap-2">
<div className="flex items-center text-text"> <div className="flex items-center text-text">
Hosts and apps Hosts and apps
<span className="ml-auto text-sm text-text-subtlest">New connections</span> <span className="ml-auto text-sm text-text-subtlest">
Changes apply to new connections.
</span>
</div> </div>
<EntryInput <EntryInput
seenRef={seenRef} seenRef={seenRef}
@@ -106,7 +109,7 @@ export function DecryptionSettings() {
/> />
<div className="rounded-md border border-border-subtle divide-y divide-border-subtle"> <div className="rounded-md border border-border-subtle divide-y divide-border-subtle">
{own.length === 0 && ( {own.length === 0 && (
<div className="px-3 py-2 text-sm text-text-subtle">Nothing listed</div> <div className="px-3 py-2 text-sm text-text-subtle">No hosts or apps</div>
)} )}
{own.map((e) => ( {own.map((e) => (
<EntryRow key={e.entry} entry={e} disabled={busy} onRemove={() => remove(e.entry)} /> <EntryRow key={e.entry} entry={e} disabled={busy} onRemove={() => remove(e.entry)} />
@@ -156,7 +159,7 @@ function CertificateRow() {
) )
} }
> >
Reveal {revealLabel(getOsType())}
</Button> </Button>
)} )}
</div> </div>
@@ -286,7 +289,7 @@ function EntryInput({
if (disabledRef.current) return true; if (disabledRef.current) return true;
const entry = normalizeEntry(text); const entry = normalizeEntry(text);
if (entry == null) { if (entry == null) {
setError("Not a host or app"); setError("Invalid host or app");
return true; return true;
} }
setError(null); setError(null);
@@ -299,20 +302,18 @@ function EntryInput({
}, [seenRef]); }, [seenRef]);
return ( return (
<Tooltip wrap content="Applies to new connections" delay={CONTROL_TOOLTIP_DELAY}> <div className="grid gap-1">
<div className="grid gap-1"> <Input
<Input label="Add entry"
label="Add entry" hideLabel
hideLabel size="sm"
size="sm" stateKey={null}
stateKey={null} language={null}
language={null} placeholder="Add host or app"
placeholder="Add host or app" extensions={extensions}
extensions={extensions} onChange={() => setError(null)}
onChange={() => setError(null)} />
/> {error != null && <div className="text-xs text-danger">{error}</div>}
{error != null && <div className="text-xs text-danger">{error}</div>} </div>
</div>
</Tooltip>
); );
} }
+2 -8
View File
@@ -126,8 +126,7 @@ export function EncryptedCard({ flow }: { flow: Flow }) {
<div className="mx-auto max-w-[27rem] py-10 grid gap-2"> <div className="mx-auto max-w-[27rem] py-10 grid gap-2">
<div className="text-sm text-text mb-1"> <div className="text-sm text-text mb-1">
<LineText line={model.why} /> <LineText line={model.why} />
{model.whySub != null && <span className="text-text-subtle"> · {model.whySub}</span>} <Tooltip wrap content={model.tooltip ?? ABOUT} delay={CONTROL_TOOLTIP_DELAY}>
<Tooltip wrap content={ABOUT} delay={CONTROL_TOOLTIP_DELAY}>
<IconButton <IconButton
size="2xs" size="2xs"
icon="info" icon="info"
@@ -137,11 +136,6 @@ export function EncryptedCard({ flow }: { flow: Flow }) {
/> />
</Tooltip> </Tooltip>
</div> </div>
{model.fact != null && (
<div className="text-sm text-text-subtle -mt-1 mb-1">
<LineText line={model.fact} />
</div>
)}
{model.actions.map((action) => ( {model.actions.map((action) => (
<ActionButton <ActionButton
key={action.label} key={action.label}
@@ -162,7 +156,7 @@ export function EncryptedCard({ flow }: { flow: Flow }) {
/> />
))} ))}
{model.foot.length > 0 && ( {model.foot.length > 0 && (
<div className="text-xs text-text-subtlest mt-0.5">{model.foot.join(" · ")}</div> <div className="text-xs text-text-subtlest mt-0.5">{model.foot.join(" ")}</div>
)} )}
{flow.error != null && flow.tlsRejected == null && ( {flow.error != null && flow.tlsRejected == null && (
<div className="text-xs text-danger mt-1">{flow.error}</div> <div className="text-xs text-danger mt-1">{flow.error}</div>
+19 -17
View File
@@ -1,4 +1,4 @@
import { Button, Icon, InlineCode, LoadingIcon } from "@yaakapp-internal/ui"; import { Button, Icon, IconTooltip, InlineCode, LoadingIcon } from "@yaakapp-internal/ui";
import classNames from "classnames"; import classNames from "classnames";
import { useAtomValue } from "jotai"; import { useAtomValue } from "jotai";
import type { ReactNode } from "react"; import type { ReactNode } from "react";
@@ -49,7 +49,7 @@ export function FirstRun() {
<div className="text-xl font-semibold mb-1.5">Set up Yaak Proxy</div> <div className="text-xl font-semibold mb-1.5">Set up Yaak Proxy</div>
<div className="text-text-subtle text-sm leading-relaxed mb-4"> <div className="text-text-subtle text-sm leading-relaxed mb-4">
{systemSupported {systemSupported
? "Two steps, once. Then every app on this Mac sends its traffic here." ? "Traffic from every app on this Mac will be recorded."
: "Traffic appears here as apps send it."} : "Traffic appears here as apps send it."}
</div> </div>
<div className="rounded-lg border border-border-subtle bg-surface divide-y divide-border-subtle"> <div className="rounded-lg border border-border-subtle bg-surface divide-y divide-border-subtle">
@@ -57,12 +57,12 @@ export function FirstRun() {
<Step <Step
n={1} n={1}
state={systemState} state={systemState}
title={systemOn ? "System proxy on" : "Turn on the system proxy"} title={systemOn ? "System proxy overridden" : "Override system proxy"}
description={ description={
otherOwner && !systemOn otherOwner && !systemOn
? proxyState?.otherOwner?.owner === "cli" ? proxyState?.otherOwner?.owner === "cli"
? "The CLI owns the proxy. Take over to turn this on from here." ? "Take over from the CLI to change this."
: "Another Yaak Proxy window owns the engine. Quit it to use this one." : "Quit the other Yaak Proxy to use this one."
: systemSwitch.description : systemSwitch.description
} }
action={ action={
@@ -73,7 +73,7 @@ export function FirstRun() {
color={systemState === "fail" ? "default" : "primary"} color={systemState === "fail" ? "default" : "primary"}
onClick={() => setSystemProxy.mutate({ enabled: true })} onClick={() => setSystemProxy.mutate({ enabled: true })}
> >
{systemState === "fail" ? "Try Again" : "Turn On"} {systemState === "fail" ? "Retry" : "Enable"}
</Button> </Button>
) : null ) : null
} }
@@ -84,20 +84,20 @@ export function FirstRun() {
state={trafficState} state={trafficState}
title={ title={
encrypting != null encrypting != null
? `Encrypting stored traffic, ${encrypting}` ? `Encrypting recorded traffic, ${encrypting}`
: trafficState === "wait" : trafficState === "wait"
? "Waiting for traffic" ? "Waiting for traffic"
: "See traffic" : "See traffic"
} }
description={ description={
encrypting != null encrypting != null
? "Traffic captured before this version is being encrypted on disk. Recording starts when it's done." ? "Recording will start once encryption finishes."
: trafficState === "fail" : trafficState === "fail"
? (retry.error?.message ?? proxyState?.startError ?? "The proxy has not started.") ? (retry.error?.message ?? proxyState?.startError ?? "The proxy failed to start.")
: !recording : !recording
? "Recording is paused, so traffic passes through without being stored." ? "Recording is paused."
: trafficState === "wait" : trafficState === "wait"
? "Open a website or send a request, and its flow appears here." ? "Open a website or send a request to begin."
: "Flows appear here as apps send requests." : "Flows appear here as apps send requests."
} }
action={ action={
@@ -123,7 +123,7 @@ export function FirstRun() {
/> />
</div> </div>
<div className="flex items-center gap-3 mt-4 text-sm text-text-subtle"> <div className="flex items-center gap-3 mt-4 text-sm text-text-subtle">
<span>Only need one terminal or one app? Copy the environment instead.</span> <span>To proxy a single terminal instead, copy the environment.</span>
<Button <Button
size="2xs" size="2xs"
variant="border" variant="border"
@@ -135,12 +135,14 @@ export function FirstRun() {
</Button> </Button>
</div> </div>
<div className="mt-2 text-sm text-text-subtle"> <div className="mt-2 text-sm text-text-subtle">
Working with an agent? <InlineCode>yaak mcp</InlineCode> gives it the proxy's flows and Agents can connect through <InlineCode>yaak mcp</InlineCode>.
actions.
</div> </div>
<div className="mt-2 text-sm text-text-subtle"> <div className="flex items-center gap-1.5 mt-2 text-sm text-text-subtle">
Captured traffic is encrypted on disk with a key kept in your keychain, so macOS may ask Recorded traffic is encrypted on disk.
to let Yaak Proxy use it. <IconTooltip
iconSize="sm"
content="The key is kept in the system keychain, so the system may ask to let Yaak Proxy use it."
/>
</div> </div>
</div> </div>
</div> </div>
+11 -4
View File
@@ -10,6 +10,7 @@ import {
CsvViewer, CsvViewer,
HexdumpViewer, HexdumpViewer,
Icon, Icon,
IconTooltip,
ImageViewer, ImageViewer,
LARGE_BODY_BYTES, LARGE_BODY_BYTES,
languageFromContentType, languageFromContentType,
@@ -238,14 +239,20 @@ export function FlowBodyViewer({ flow, which, form, hideCutNote = false }: Props
<div className="h-full grid grid-rows-[auto_auto_minmax(0,1fr)]"> <div className="h-full grid grid-rows-[auto_auto_minmax(0,1fr)]">
{decodeError != null && ( {decodeError != null && (
<Banner color="danger" className="text-sm mb-2"> <Banner color="danger" className="text-sm mb-2">
{decodeError}. Showing the body as it was on the wire. {decodeError}. The body is shown as it was received.
</Banner> </Banner>
)} )}
{truncated && !hideCutNote && ( {truncated && !hideCutNote && (
<Banner color="notice" className="text-sm mb-2"> <Banner color="notice" className="text-sm mb-2">
{wireTruncated || decodeError != null <span className="flex items-center gap-1.5">
? `Showing the first ${formatSize(storedSize)} of ${formatSize(wireSize)}, the capture limit. The app received all of it.` {wireTruncated || decodeError != null
: `Showing the first ${formatSize(bytes.length)} of the decoded body, the capture limit. The app received all of it.`} ? `The first ${formatSize(storedSize)} of ${formatSize(wireSize)} is shown.`
: `The first ${formatSize(bytes.length)} of the decoded body is shown.`}
<IconTooltip
iconSize="sm"
content="The body was truncated at the capture limit. The app received all of it."
/>
</span>
</Banner> </Banner>
)} )}
<div className="min-h-0 row-start-3">{viewer}</div> <div className="min-h-0 row-start-3">{viewer}</div>
+3 -1
View File
@@ -323,7 +323,9 @@ function FlowResponseHalf({ flow, className }: { flow: Flow; className: string }
)} )}
> >
<Icon icon="scissors" size="xs" /> <Icon icon="scissors" size="xs" />
{body.data == null ? "Body cut" : `First ${formatSize(body.data.storedSize)} kept`} {body.data == null
? "Body truncated"
: `First ${formatSize(body.data.storedSize)} recorded`}
</span> </span>
) : undefined ) : undefined
} }
+4 -4
View File
@@ -65,8 +65,8 @@ export function FlowInfo({ flow }: { flow: Flow }) {
"Started", "Started",
format(utcTimestamp(flow.startedAt), "MMM d, yyyy, h:mm:ss.SSS a"), format(utcTimestamp(flow.startedAt), "MMM d, yyyy, h:mm:ss.SSS a"),
), ),
row("headers", "Headers", ms(flow.elapsedHeadersMs)), row("headers", "Latency", ms(flow.elapsedHeadersMs)),
row("total", "Total", ms(flow.elapsedMs)), row("total", "Time", ms(flow.elapsedMs)),
]} ]}
</Section> </Section>
<Section title="Size"> <Section title="Size">
@@ -75,11 +75,11 @@ export function FlowInfo({ flow }: { flow: Flow }) {
row("response_size", "Response", formatSize(flow.responseSize)), row("response_size", "Response", formatSize(flow.responseSize)),
]} ]}
</Section> </Section>
<Section title="Record"> <Section title="Flow">
{[ {[
row("number", "Number", `#${flow.number}`, { copy: true }), row("number", "Number", `#${flow.number}`, { copy: true }),
row("id", "ID", flow.id, { copy: true }), row("id", "ID", flow.id, { copy: true }),
row("saved", "Saved As", flow.savedRequestId, { copy: true }), row("saved", "Yaak Request ID", flow.savedRequestId, { copy: true }),
]} ]}
</Section> </Section>
<Timeline flow={flow} /> <Timeline flow={flow} />
+6 -6
View File
@@ -914,9 +914,9 @@ function encryptedReason(flow: Flow): string {
? "Encrypted: excluded by default" ? "Encrypted: excluded by default"
: `Encrypted: excluded by ${flow.tunnelExcludedBy}`; : `Encrypted: excluded by ${flow.tunnelExcludedBy}`;
case "rejected": case "rejected":
return "Encrypted: passed through after a client rejected the certificate"; return "Encrypted: an app rejected the certificate";
default: default:
return "Encrypted: not on the decrypt list"; return "Encrypted: decryption is not enabled";
} }
} }
@@ -929,12 +929,12 @@ function FlowMark({ flow }: { flow: Flow }) {
const open = isOpenStream(flow); const open = isOpenStream(flow);
const rejected = flow.tlsRejected != null; const rejected = flow.tlsRejected != null;
const states: string[] = []; const states: string[] = [];
if (rejected) states.push("Rejected the proxy's certificate"); if (rejected) states.push("Certificate rejected by the app");
else if (flow.error != null) states.push(`Failed: ${flow.error}`); else if (flow.error != null) states.push(`Failed: ${flow.error}`);
if (flow.cancelled) states.push("Cancelled by the app"); if (flow.cancelled) states.push("Cancelled by the app");
if (flow.tunnelled) states.push(encryptedReason(flow)); if (flow.tunnelled) states.push(encryptedReason(flow));
if (open) states.push("Streaming"); if (open) states.push("Streaming");
if (flow.responseTruncated) states.push("Body cut at the capture limit"); if (flow.responseTruncated) states.push("Body truncated at the capture limit");
if (flow.savedRequestId != null) states.push("Saved to Yaak"); if (flow.savedRequestId != null) states.push("Saved to Yaak");
const mark = ( const mark = (
<span className="flex items-center justify-center"> <span className="flex items-center justify-center">
@@ -1369,9 +1369,9 @@ function useCellActions(): (flow: Flow, column: ColumnId) => DropdownItem[] {
onSelect: async () => { onSelect: async () => {
try { try {
await navigator.clipboard.writeText(value); await navigator.clipboard.writeText(value);
showToast({ message: "Copied value" }); showToast({ message: "Copied to clipboard" });
} catch (err) { } catch (err) {
showToast({ message: `Failed to copy value: ${String(err)}`, color: "danger" }); showToast({ message: `Failed to copy: ${String(err)}`, color: "danger" });
} }
}, },
}); });
+7 -10
View File
@@ -31,18 +31,17 @@ export function FlowListEmpty() {
<div className="text-text text-sm">{recording ? "No flows yet" : "Recording is paused"}</div> <div className="text-text text-sm">{recording ? "No flows yet" : "Recording is paused"}</div>
<div className="text-sm text-text-subtle max-w-[26rem]"> <div className="text-sm text-text-subtle max-w-[26rem]">
{!running {!running
? "The proxy is not running. Retry from the menu by the state." ? "Retry from the status menu to start the proxy."
: !recording : !recording
? "Traffic passes through without being stored." ? "Traffic passes through without being recorded."
: `Recording on port ${port ?? "--"}. Flows appear as apps send requests.`} : `Traffic sent to port ${port ?? "--"} will be recorded.`}
</div> </div>
</EmptyStateText> </EmptyStateText>
); );
} }
/** /**
* The list's last row while recording is paused, where the next flow would appear: since when, * The list's last row while recording is paused, where the next flow would appear: since when.
* and that nothing is stored.
*/ */
export function PausedRow({ style }: { style?: React.CSSProperties }) { export function PausedRow({ style }: { style?: React.CSSProperties }) {
const pausedAt = useAtomValue(pausedAtAtom); const pausedAt = useAtomValue(pausedAtAtom);
@@ -56,8 +55,7 @@ export function PausedRow({ style }: { style?: React.CSSProperties }) {
> >
<Icon icon="circle_pause" size="sm" className="text-text-subtlest" /> <Icon icon="circle_pause" size="sm" className="text-text-subtlest" />
<span className="truncate"> <span className="truncate">
{pausedAt == null ? "Paused." : `Paused at ${timeOf(pausedAt)}.`} Traffic passes through {pausedAt == null ? "Recording is paused." : `Recording was paused at ${timeOf(pausedAt)}.`}
without being stored.
</span> </span>
<Hotkey action="recording_toggle" variant="text" className="ml-auto" /> <Hotkey action="recording_toggle" variant="text" className="ml-auto" />
</div> </div>
@@ -102,11 +100,10 @@ function NoResults({ text }: { text: string }) {
className="not-italic !h-auto py-3 px-4 text-sm text-text-subtle" className="not-italic !h-auto py-3 px-4 text-sm text-text-subtle"
> >
<div className="text-center leading-relaxed"> <div className="text-center leading-relaxed">
No flows match. {wider.length === 0 ? "No results" : "No results, but found matches for "}
{wider.length > 0 && " Found matches for "}
{wider.map((w, i) => ( {wider.map((w, i) => (
<span key={w.text}> <span key={w.text}>
{i > 0 && (i === wider.length - 1 ? ", or " : ", ")} {i > 0 && " or "}
<button type="button" className="hover:underline" onClick={() => setText(w.text)}> <button type="button" className="hover:underline" onClick={() => setText(w.text)}>
<InlineCode>{w.text === "" ? "all traffic" : w.text}</InlineCode> <InlineCode>{w.text === "" ? "all traffic" : w.text}</InlineCode>
</button>{" "} </button>{" "}
@@ -159,7 +159,7 @@ export const FlowListHeader = memo(function FlowListHeader({
: {}), : {}),
}; };
}), }),
{ label: "Add Column…", icon: "plus", onSelect: () => openAddColumn(true) }, { label: "Add Column", icon: "plus", onSelect: () => openAddColumn(true) },
{ type: "separator" }, { type: "separator" },
{ label: "Reset Sort", disabled: sort.length === 0, onSelect: onResetSort }, { label: "Reset Sort", disabled: sort.length === 0, onSelect: onResetSort },
{ label: "Reset Column Widths", onSelect: () => setPrefs((p) => resetWidths(p)) }, { label: "Reset Column Widths", onSelect: () => setPrefs((p) => resetWidths(p)) },
@@ -179,7 +179,7 @@ export const FlowListHeader = memo(function FlowListHeader({
{ label: "Sort Descending", onSelect: () => sortBy(true) }, { label: "Sort Descending", onSelect: () => sortBy(true) },
{ label: "Add to Sort Ascending", onSelect: () => addTo(false) }, { label: "Add to Sort Ascending", onSelect: () => addTo(false) },
{ label: "Add to Sort Descending", onSelect: () => addTo(true) }, { label: "Add to Sort Descending", onSelect: () => addTo(true) },
{ label: "Clear Sorting", disabled: sort.length === 0, onSelect: onResetSort }, { label: "Reset Sort", disabled: sort.length === 0, onSelect: onResetSort },
{ type: "separator" }, { type: "separator" },
{ {
label: "Move Left", label: "Move Left",
+3 -1
View File
@@ -120,7 +120,9 @@ export function FlowMessages({ flow }: { flow: Flow }) {
header={header} header={header}
error={error ?? (open ? null : flow.error)} error={error ?? (open ? null : flow.error)}
isLoading={loading && events.length === 0} isLoading={loading && events.length === 0}
emptyMessage={open ? `Waiting for the first ${noun.toLowerCase()}` : `No ${noun}s`} emptyMessage={
open ? `Waiting for the first ${noun.toLowerCase()}` : `No ${noun.toLowerCase()}s`
}
splitLayoutStorageKey="proxy_flow_messages" splitLayoutStorageKey="proxy_flow_messages"
columns={columns} columns={columns}
loadPayload={loadPayload} loadPayload={loadPayload}
@@ -4,15 +4,14 @@ import { describe, expect, test, vi } from "vite-plus/test";
import type { FlowBodyData } from "../lib/flowViews"; import type { FlowBodyData } from "../lib/flowViews";
import { FlowRequestBody } from "./FlowRequestBody"; import { FlowRequestBody } from "./FlowRequestBody";
// The package's index reaches for the window as it loads; these three are all this renders. // The package's index reaches for the window as it loads; these four are all this renders.
vi.mock("@yaakapp-internal/ui", () => ({ vi.mock("@yaakapp-internal/ui", () => ({
Banner: ({ children, color }: { children: ReactNode; color?: string }) => ( Banner: ({ children, color }: { children: ReactNode; color?: string }) => (
<div data-banner={color}>{children}</div> <div data-banner={color}>{children}</div>
), ),
EmptyStateText: ({ children }: { children: ReactNode }) => <div>{children}</div>, EmptyStateText: ({ children }: { children: ReactNode }) => <div>{children}</div>,
LoadingIcon: () => <span>LOADING</span>, LoadingIcon: () => <span>LOADING</span>,
CONTROL_TOOLTIP_DELAY: 0, IconTooltip: ({ content }: { content: string }) => <span data-tooltip={content} />,
Tooltip: ({ children }: { children: ReactNode }) => <>{children}</>,
})); }));
const data = (truncated = false): FlowBodyData => ({ const data = (truncated = false): FlowBodyData => ({
@@ -45,10 +44,13 @@ describe("FlowRequestBody", () => {
expect(render({ data: data(), isLoading: false, error: null })).toContain("EDITOR"); expect(render({ data: data(), isLoading: false, error: null })).toContain("EDITOR");
}); });
test("a cut body says how much was kept", () => { test("a truncated body says how much is shown, and why behind its info icon", () => {
const html = render({ data: data(true), isLoading: false, error: null }); const html = render({ data: data(true), isLoading: false, error: null });
expect(html).toContain("EDITOR"); expect(html).toContain("EDITOR");
expect(html).toContain('data-banner="notice"'); expect(html).toContain('data-banner="notice"');
expect(html).toContain("kept"); expect(html.replaceAll("<!-- -->", "")).toContain("The first 7 B of 2 KB is shown.");
expect(html).toContain(
'data-tooltip="The body was truncated at the capture limit. The app sent all of it."',
);
}); });
}); });
+8 -12
View File
@@ -1,11 +1,5 @@
import { formatSize } from "@yaakapp-internal/lib/formatSize"; import { formatSize } from "@yaakapp-internal/lib/formatSize";
import { import { Banner, EmptyStateText, IconTooltip, LoadingIcon } from "@yaakapp-internal/ui";
Banner,
CONTROL_TOOLTIP_DELAY,
EmptyStateText,
LoadingIcon,
Tooltip,
} from "@yaakapp-internal/ui";
import type { HttpBodyResult } from "@yaakapp-internal/ui/httpTypes"; import type { HttpBodyResult } from "@yaakapp-internal/ui/httpTypes";
import type { ReactNode } from "react"; import type { ReactNode } from "react";
import type { FlowBodyData } from "../lib/flowViews"; import type { FlowBodyData } from "../lib/flowViews";
@@ -42,11 +36,13 @@ export function FlowRequestBody({ body, viewer, requestSize }: Props) {
return ( return (
<div className="h-full grid grid-rows-[auto_minmax(0,1fr)]"> <div className="h-full grid grid-rows-[auto_minmax(0,1fr)]">
<Banner color="notice" className="text-sm mb-2"> <Banner color="notice" className="text-sm mb-2">
<Tooltip wrap content="The app sent all of it" delay={CONTROL_TOOLTIP_DELAY}> <span className="flex items-center gap-1.5">
<span> The first {formatSize(body.data.storedSize)} of {formatSize(requestSize)} is shown.
First {formatSize(body.data.storedSize)} of {formatSize(requestSize)} kept <IconTooltip
</span> iconSize="sm"
</Tooltip> content="The body was truncated at the capture limit. The app sent all of it."
/>
</span>
</Banner> </Banner>
<div className="min-h-0">{viewer}</div> <div className="min-h-0">{viewer}</div>
</div> </div>
+2 -2
View File
@@ -48,8 +48,8 @@ export function HelperDialog() {
open={open} open={open}
onClose={close} onClose={close}
size="dynamic" size="dynamic"
title="Turn on Yaak Proxy under Allow in the Background" title="Approve Helper"
description="In System Settings > General > Login Items. Newer macOS calls the list Background App Activity." description="Turn on Yaak Proxy in System Settings > General > Login Items."
className="w-[34rem]" className="w-[34rem]"
> >
<div className="grid gap-3 pb-4"> <div className="grid gap-3 pb-4">
+9 -9
View File
@@ -24,7 +24,7 @@ const STRIP =
/** /**
* Above the list while nothing is decrypted: says once that HTTPS is encrypted, and offers * Above the list while nothing is decrypted: says once that HTTPS is encrypted, and offers
* Decrypt Everything, or Choose… for apps and sites one at a time. Goes once anything is on * Decrypt Everything, or Choose for apps and sites one at a time. Goes once anything is on
* the list, or when dismissed; comes back, with Trust, while the list decrypts something the * the list, or when dismissed; comes back, with Trust, while the list decrypts something the
* certificate isn't trusted for. * certificate isn't trusted for.
*/ */
@@ -33,7 +33,7 @@ export function HttpsStrip() {
const flows = useAtomValue(sidebarFlowsAtom); const flows = useAtomValue(sidebarFlowsAtom);
const caStatus = useRpcQuery("ca_status", {}); const caStatus = useRpcQuery("ca_status", {});
const [dismissed, setDismissed] = useLocalStorage("https_strip_dismissed", false); const [dismissed, setDismissed] = useLocalStorage("https_strip_dismissed", false);
// Kept while Choose… is open, so checking a row there leaves the menu where it is. // Kept while Choose is open, so checking a row there leaves the menu where it is.
const [choosing, setChoosing] = useState(false); const [choosing, setChoosing] = useState(false);
const { apply } = useDecrypt(); const { apply } = useDecrypt();
const trusting = useTrusting(); const trusting = useTrusting();
@@ -56,7 +56,7 @@ export function HttpsStrip() {
<div className={STRIP}> <div className={STRIP}>
<Icon icon="shield_off" size="sm" color="warning" className="shrink-0" /> <Icon icon="shield_off" size="sm" color="warning" className="shrink-0" />
<span className="text-text shrink-0">Certificate not trusted</span> <span className="text-text shrink-0">Certificate not trusted</span>
<span className="truncate">Required to decrypt</span> <span className="truncate">The certificate must be trusted to decrypt traffic.</span>
<Button <Button
size="2xs" size="2xs"
color="primary" color="primary"
@@ -93,7 +93,7 @@ export function HttpsStrip() {
}); });
}, },
})), })),
{ type: "separator", label: "Sites", hidden: options.sites.length === 0 }, { type: "separator", label: "Domains", hidden: options.sites.length === 0 },
...options.sites.map((site): DropdownItem => ({ ...options.sites.map((site): DropdownItem => ({
label: <span className="font-mono text-xs">*.{site.site}</span>, label: <span className="font-mono text-xs">*.{site.site}</span>,
keepOpenOnSelect: true, keepOpenOnSelect: true,
@@ -109,7 +109,7 @@ export function HttpsStrip() {
})), })),
{ type: "separator" }, { type: "separator" },
{ {
label: "Edit List in Settings…", label: "Decryption Settings",
leftSlot: <Icon icon="settings" />, leftSlot: <Icon icon="settings" />,
onSelect: () => openSettings(SETTINGS_TAB_PROXY), onSelect: () => openSettings(SETTINGS_TAB_PROXY),
}, },
@@ -124,7 +124,7 @@ export function HttpsStrip() {
<div className="ml-auto flex items-center gap-1.5 shrink-0"> <div className="ml-auto flex items-center gap-1.5 shrink-0">
<Tooltip <Tooltip
wrap wrap
content="New connections, except Apple services and apps that pin" content="New connections will be decrypted, except for excluded hosts and apps."
delay={CONTROL_TOOLTIP_DELAY} delay={CONTROL_TOOLTIP_DELAY}
> >
<Button <Button
@@ -136,7 +136,7 @@ export function HttpsStrip() {
void apply({ void apply({
edit: everythingEdit(true), edit: everythingEdit(true),
message: "Decrypting everything", message: "Decrypting everything",
sub: "Except Apple services and apps that pin", sub: "Excluded hosts and apps stay encrypted.",
}) })
} }
> >
@@ -145,14 +145,14 @@ export function HttpsStrip() {
</Tooltip> </Tooltip>
<Dropdown items={items} onOpen={() => setChoosing(true)} onClose={() => setChoosing(false)}> <Dropdown items={items} onOpen={() => setChoosing(true)} onClose={() => setChoosing(false)}>
<Button size="2xs" variant="border"> <Button size="2xs" variant="border">
Choose… Choose
</Button> </Button>
</Dropdown> </Dropdown>
<IconButton <IconButton
size="xs" size="xs"
icon="x" icon="x"
iconColor="secondary" iconColor="secondary"
title="Not now" title="Dismiss"
tooltip tooltip
onClick={() => setDismissed(true)} onClick={() => setDismissed(true)}
/> />
+1 -1
View File
@@ -28,7 +28,7 @@ export function LockedState({ lock }: { lock: ProxyLock }) {
<div className="text-lg font-semibold whitespace-nowrap"> <div className="text-lg font-semibold whitespace-nowrap">
{encrypting != null ? ( {encrypting != null ? (
<> <>
Encrypting stored traffic,{" "} Encrypting recorded traffic,{" "}
{/* Sized for "100%" and right-aligned, so the centred line never changes width as {/* Sized for "100%" and right-aligned, so the centred line never changes width as
the number ticks: a changing width re-rasterizes every glyph at a new the number ticks: a changing width re-rasterizes every glyph at a new
sub-pixel offset, and the line shakes */} sub-pixel offset, and the line shakes */}
+14 -17
View File
@@ -65,24 +65,20 @@ export function ProxyStatus() {
status === "encrypting" ? ( status === "encrypting" ? (
<MenuRow <MenuRow
icon={<LoadingIcon size="sm" className="text-text-subtle" />} icon={<LoadingIcon size="sm" className="text-text-subtle" />}
title={`Encrypting stored traffic, ${encrypting}`} title={`Encrypting recorded traffic, ${encrypting}`}
description="Traffic captured before this version is being encrypted on disk. Recording starts when it's done." description="Recording will start once encryption finishes."
/> />
) : status === "cli" && other != null ? ( ) : status === "cli" && other != null ? (
<MenuRow <MenuRow
icon={<StateDot state="cli" />} icon={<StateDot state="cli" />}
title="Running from the CLI" title="Running from the CLI"
description={`Started by yaak proxy start, pid ${other.pid}, since ${timeOf(other.startedAt)}.${other.proxyPort == null ? "" : ` Port ${other.proxyPort}.`}`} description={`The proxy was started by the CLI at ${timeOf(other.startedAt)}.`}
/> />
) : status === "recording" || status === "paused" ? ( ) : status === "recording" || status === "paused" ? (
<MenuRow <MenuRow
icon={<StateDot state={status} />} icon={<StateDot state={status} />}
title={recording ? `Recording on port ${port}` : `Paused on port ${port}`} title={recording ? `Recording on port ${port}` : `Paused on port ${port}`}
description={ description={recording ? null : "Traffic passes through without being recorded."}
recording
? "Every flow is stored as it passes."
: "Traffic passes through untouched, and nothing is stored."
}
/> />
) : ( ) : (
<MenuRow <MenuRow
@@ -90,7 +86,7 @@ export function ProxyStatus() {
title="Not running" title="Not running"
description={ description={
<span className="text-danger"> <span className="text-danger">
{retry.error?.message ?? proxyState?.startError ?? "The proxy has not started."} {retry.error?.message ?? proxyState?.startError ?? "The proxy failed to start."}
</span> </span>
} }
action={ action={
@@ -156,9 +152,7 @@ export function ProxyStatus() {
{systemSwitch.description} {systemSwitch.description}
</span> </span>
{helperNotice && ( {helperNotice && (
<span className="block text-notice"> <span className="block text-notice">macOS will ask for a password each time.</span>
macOS asks for your password each time until the helper is approved in Settings.
</span>
)} )}
</> </>
} }
@@ -258,8 +252,8 @@ function DecryptEverythingRow({ broken }: { broken: boolean }) {
trusting trusting
? "Waiting for macOS" ? "Waiting for macOS"
: on : on
? "Except Apple services and apps that pin" ? "Excluded hosts and apps stay encrypted."
: (listSummary(list) ?? "Nothing listed") : (listSummary(list) ?? "No hosts or apps")
} }
action={ action={
<KeepMenuOpen> <KeepMenuOpen>
@@ -274,7 +268,7 @@ function DecryptEverythingRow({ broken }: { broken: boolean }) {
? { ? {
edit: everythingEdit(true), edit: everythingEdit(true),
message: "Decrypting everything", message: "Decrypting everything",
sub: "Except Apple services and apps that pin", sub: "Excluded hosts and apps stay encrypted.",
} }
: { edit: everythingEdit(false), message: "Stopped decrypting everything" }, : { edit: everythingEdit(false), message: "Stopped decrypting everything" },
) )
@@ -350,7 +344,7 @@ export function useCopyEnvironment() {
try { try {
await navigator.clipboard.writeText(environmentExports(port, caPath)); await navigator.clipboard.writeText(environmentExports(port, caPath));
showToast({ showToast({
message: "Copied the environment. Paste it into a terminal to proxy what runs there.", message: "Copied the environment. Paste it into a terminal.",
}); });
} catch (err) { } catch (err) {
showToast({ message: `Failed to copy: ${String(err)}`, color: "danger" }); showToast({ message: `Failed to copy: ${String(err)}`, color: "danger" });
@@ -429,6 +423,7 @@ function MenuRow({
}: { }: {
icon: ReactNode; icon: ReactNode;
title: string; title: string;
/** Under the title; null for a title that says it all */
description: ReactNode; description: ReactNode;
action?: ReactNode; action?: ReactNode;
}) { }) {
@@ -437,7 +432,9 @@ function MenuRow({
<span className="h-5 flex items-center justify-center">{icon}</span> <span className="h-5 flex items-center justify-center">{icon}</span>
<div> <div>
<div className="text-sm text-text">{title}</div> <div className="text-sm text-text">{title}</div>
<div className="text-xs text-text-subtle mt-0.5">{description}</div> {description != null && (
<div className="text-xs text-text-subtle mt-0.5">{description}</div>
)}
</div> </div>
<span className="self-center">{action}</span> <span className="self-center">{action}</span>
</div> </div>
+20 -24
View File
@@ -40,8 +40,8 @@ export function SaveDialog() {
open open
onClose={close} onClose={close}
size="dynamic" size="dynamic"
title="Save as Yaak request" title="Save as Yaak Request"
description="A new request in Yaak with this flow's method, URL, headers and body." description="A new request will be created in Yaak."
className="w-[36rem]" className="w-[36rem]"
> >
{workspaces.error != null ? ( {workspaces.error != null ? (
@@ -103,7 +103,7 @@ function SaveForm({
message: ( message: (
<> <>
Saved <b className="font-semibold">{saved.name || unnamed}</b> to {workspace.name} Saved <b className="font-semibold">{saved.name || unnamed}</b> to {workspace.name}
{folderName == null ? "." : `, in ${folderName}.`} Yaak picks it up within a second. {folderName == null ? "" : `, in ${folderName}`}
</> </>
), ),
}); });
@@ -113,11 +113,7 @@ function SaveForm({
}; };
if (workspaces.length === 0) { if (workspaces.length === 0) {
return ( return <Banner color="notice">Create a workspace in Yaak first.</Banner>;
<Banner color="notice">
Yaak has no workspaces yet. Create one in Yaak, then save here.
</Banner>
);
} }
if (p == null) { if (p == null) {
@@ -209,13 +205,10 @@ function SaveForm({
</div> </div>
{save.error != null && <Banner color="danger">{save.error.message}</Banner>} {save.error != null && <Banner color="danger">{save.error.message}</Banner>}
<DialogFooter <DialogFooter
leftSlot={
<span className="text-xs text-text-subtlest">Yaak picks it up within a second</span>
}
actions={[ actions={[
{ label: "Cancel", onClick: onClose }, { label: "Cancel", onClick: onClose },
{ {
label: "Save request", label: "Save Request",
form: SAVE_FORM_ID, form: SAVE_FORM_ID,
color: "primary", color: "primary",
isLoading: save.isPending, isLoading: save.isPending,
@@ -243,15 +236,19 @@ function NoYaakDialog({ flow, onClose }: { flow: Flow; onClose: () => void }) {
return ( return (
<Dialog open onClose={onClose} size="dynamic" className="w-[32rem]"> <Dialog open onClose={onClose} size="dynamic" className="w-[32rem]">
<DialogFooter <DialogFooter
leftSlot={<span className="text-xs text-text-subtlest">Or keep the flow as a file</span>} leftSlot={
<span className="text-xs text-text-subtlest">
The flow can be exported as a file instead.
</span>
}
actions={[ actions={[
{ {
label: "Export HAR", label: "Export as HAR",
onClick: async () => { onClick: async () => {
try { try {
if ((await exportHar({ ids: [flow.id] }, "flow.har")) != null) { if ((await exportHar({ ids: [flow.id] }, "flow.har")) != null) {
onClose(); onClose();
showToast({ message: "Exported the flow as HAR" }); showToast({ message: "Exported 1 flow as HAR" });
} }
} catch (err) { } catch (err) {
showToast({ message: `Failed to export HAR: ${String(err)}`, color: "danger" }); showToast({ message: `Failed to export HAR: ${String(err)}`, color: "danger" });
@@ -262,11 +259,10 @@ function NoYaakDialog({ flow, onClose }: { flow: Flow; onClose: () => void }) {
]} ]}
/> />
<div className="pt-6 pb-2"> <div className="pt-6 pb-2">
<div className="text-lg font-semibold mb-1.5">Saving needs Yaak</div> <div className="text-lg font-semibold mb-1.5">Yaak Required</div>
<div className="text-sm text-text-subtle leading-relaxed"> <div className="text-sm text-text-subtle leading-relaxed">
This flow becomes a request in one of Yaak's workspaces, and Yaak has not been opened on This flow will be saved as a request in one of Yaak's workspaces, but Yaak has not been
this Mac. Yaak is free for personal use and opens the request with the same viewers you opened on this computer.
see here.
</div> </div>
</div> </div>
</Dialog> </Dialog>
@@ -308,18 +304,18 @@ function folderOptions(folders: FolderSummary[]): { id: string; name: string }[]
function reasonLabel(reason: DropReason): string { function reasonLabel(reason: DropReason): string {
switch (reason) { switch (reason) {
case "set_by_sending": case "set_by_sending":
return "Yaak sets it when it sends"; return "Yaak sets it when it sends the request.";
case "hop_by_hop": case "hop_by_hop":
return "Only meant for the proxy"; return "It was only meant for the proxy.";
case "body_decoded": case "body_decoded":
return "The body is saved decoded"; return "The body is saved decoded.";
} }
} }
function droppedSummary(reasons: DropReason[]): string { function droppedSummary(reasons: DropReason[]): string {
const unique = [...new Set(reasons)]; const unique = [...new Set(reasons)];
if (unique.length === 1 && unique[0] === "set_by_sending") { if (unique.length === 1 && unique[0] === "set_by_sending") {
return "Yaak sets these itself when it sends"; return "Yaak sets these itself when it sends the request.";
} }
return "Yaak sets these itself, or they only meant something to the proxy"; return "Yaak sets these, or they were meant for the proxy.";
} }
@@ -47,7 +47,7 @@ function NameDialog({ dialog, onClose }: { dialog: SavedSearchDialog; onClose: (
open open
onClose={onClose} onClose={onClose}
size="dynamic" size="dynamic"
title={dialog.mode === "save" ? "Save search" : "Rename search"} title={dialog.mode === "save" ? "Save Search" : "Rename Search"}
className="w-[26rem]" className="w-[26rem]"
> >
<form <form
+3 -3
View File
@@ -205,7 +205,7 @@ export const SearchBar = memo(function SearchBar() {
<Dropdown <Dropdown
items={[ items={[
{ {
label: "Save search", label: "Save Search",
leftSlot: <Icon icon="bookmark" />, leftSlot: <Icon icon="bookmark" />,
onSelect: () => onSelect: () =>
setSavedDialog({ setSavedDialog({
@@ -275,7 +275,7 @@ function FilterHistory({
const items: DropdownItem[] = const items: DropdownItem[] =
history.length === 0 history.length === 0
? [{ label: "No recent filters", disabled: true }] ? [{ label: "No recent searches", disabled: true }]
: [ : [
...history.map((filter): DropdownItem => ({ ...history.map((filter): DropdownItem => ({
label: ( label: (
@@ -311,7 +311,7 @@ function FilterHistory({
<IconButton <IconButton
size="xs" size="xs"
icon="search" icon="search"
title="Recent filters" title="Recent searches"
tooltip tooltip
iconColor="secondary" iconColor="secondary"
disabled={disabled} disabled={disabled}
+1 -1
View File
@@ -235,7 +235,7 @@ function useAppInfo() {
? "Reveal in Finder" ? "Reveal in Finder"
: os === "windows" : os === "windows"
? "Show in Explorer" ? "Show in Explorer"
: "Show in Folder", : "Show in File Manager",
}), }),
[data, os], [data, os],
); );
+28 -22
View File
@@ -6,6 +6,7 @@ import {
Dialog, Dialog,
DialogFooter, DialogFooter,
Icon, Icon,
IconTooltip,
RadioDropdown, RadioDropdown,
SettingRow, SettingRow,
SettingRowBoolean, SettingRowBoolean,
@@ -58,27 +59,30 @@ export function SettingsProxy() {
const elsewhere = proxyState?.otherOwner != null; const elsewhere = proxyState?.otherOwner != null;
return ( return (
<SettingsPage title="Proxy" description="Listening, decryption, traffic and terminals"> <SettingsPage
title="Proxy"
description="Configure the port, decryption, recorded traffic, and terminal environment."
>
<SettingsList className="space-y-8"> <SettingsList className="space-y-8">
<SettingsSection title="Proxy"> <SettingsSection title="Proxy">
<PortRow disabled={elsewhere} /> <PortRow disabled={elsewhere} />
<SettingRowSelect<HttpVersion> <SettingRowSelect<HttpVersion>
name="httpVersion" name="httpVersion"
title="HTTP version" title="HTTP version"
description="For new connections" description="The change applies to new connections."
value={proxyState?.httpVersion ?? "auto"} value={proxyState?.httpVersion ?? "auto"}
disabled={elsewhere} disabled={elsewhere}
onChange={(httpVersion) => setHttpVersion.mutate({ httpVersion })} onChange={(httpVersion) => setHttpVersion.mutate({ httpVersion })}
options={[ options={[
{ label: "Auto", value: "auto" }, { label: "Automatic", value: "auto" },
{ label: "HTTP/1.1 only", value: "http1Only" }, { label: "HTTP/1.1 only", value: "http1Only" },
]} ]}
/> />
{systemProxy.data?.supported === true && ( {systemProxy.data?.supported === true && (
<> <>
<SettingRowBoolean <SettingRowBoolean
title="Override system proxy" title="Override system proxy at launch"
description="At launch" description="Traffic from every app will be recorded."
checked={proxyState?.systemProxyAtLaunch ?? true} checked={proxyState?.systemProxyAtLaunch ?? true}
disabled={setAtLaunch.isPending} disabled={setAtLaunch.isPending}
onChange={(enabled) => setAtLaunch.mutate({ enabled })} onChange={(enabled) => setAtLaunch.mutate({ enabled })}
@@ -147,7 +151,7 @@ function PortRow({ disabled }: { disabled: boolean }) {
setPort.mutate({ port }); setPort.mutate({ port });
}; };
return ( return (
<SettingRow title="Port" description="Restarts the proxy"> <SettingRow title="Port" description="Changing the port restarts the proxy.">
<PlainInput <PlainInput
hideLabel hideLabel
size="sm" size="sm"
@@ -184,7 +188,10 @@ function HelperRow({ status }: { status: SystemProxyStatus }) {
unregister.error != null ? ( unregister.error != null ? (
<span className="text-danger">{unregister.error.message}</span> <span className="text-danger">{unregister.error.message}</span>
) : ( ) : (
`${capitalize(row.title.replace(/^Helper: /, ""))}. ${row.description}` <span className="inline-flex items-center gap-1.5">
{row.description}
<IconTooltip iconSize="sm" content={row.tooltip} />
</span>
) )
} }
> >
@@ -200,7 +207,7 @@ function HelperRow({ status }: { status: SystemProxyStatus }) {
? "Install" ? "Install"
: row.action === "approve" : row.action === "approve"
? "Approve" ? "Approve"
: "Unregister"} : "Uninstall"}
</Button> </Button>
)} )}
</SettingRow> </SettingRow>
@@ -217,7 +224,7 @@ function TrafficRow() {
return ( return (
<SettingRowSelect<TrafficRetention> <SettingRowSelect<TrafficRetention>
name="traffic" name="traffic"
title="Captured traffic" title="Recorded traffic"
selectClassName="w-56!" selectClassName="w-56!"
value={proxyState?.traffic ?? "clearOnLaunch"} value={proxyState?.traffic ?? "clearOnLaunch"}
disabled={proxyState?.otherOwner != null} disabled={proxyState?.otherOwner != null}
@@ -240,7 +247,7 @@ function KeepTrafficRow() {
}); });
const keep = proxyState?.keepTrafficBytes ?? null; const keep = proxyState?.keepTrafficBytes ?? null;
return ( return (
<SettingRow title="Size limit" description="Oldest flows dropped past it"> <SettingRow title="Size limit" description="The oldest flows will be cleared first.">
<div className="flex items-center gap-3"> <div className="flex items-center gap-3">
{size.data != null && ( {size.data != null && (
<span className="text-sm text-text-subtle tabular-nums whitespace-nowrap"> <span className="text-sm text-text-subtle tabular-nums whitespace-nowrap">
@@ -280,21 +287,24 @@ function ResetRow() {
onSuccess: () => { onSuccess: () => {
setConfirming(false); setConfirming(false);
void queryClient.invalidateQueries(); void queryClient.invalidateQueries();
showToast({ message: "Captured data reset" }); showToast({ message: "Recorded data reset" });
}, },
onSettled: () => void refreshProxyState(store), onSettled: () => void refreshProxyState(store),
}); });
return ( return (
<SettingRow title="Reset captured data" description="Deletes all flows and their keys"> <SettingRow
title="Reset recorded data"
description="All traffic and its encryption keys will be deleted."
>
<Button size="xs" variant="border" color="danger" onClick={() => setConfirming(true)}> <Button size="xs" variant="border" color="danger" onClick={() => setConfirming(true)}>
Reset… Reset
</Button> </Button>
<Dialog <Dialog
open={confirming} open={confirming}
onClose={() => setConfirming(false)} onClose={() => setConfirming(false)}
size="sm" size="sm"
title="Reset captured data?" title="Reset Recorded Data"
description="Every captured flow and the keys that encrypt them are deleted." description="Permanently delete all traffic and its encryption keys?"
> >
{reset.error != null && <p className="text-danger text-sm">{reset.error.message}</p>} {reset.error != null && <p className="text-danger text-sm">{reset.error.message}</p>}
<DialogFooter <DialogFooter
@@ -351,8 +361,8 @@ function TerminalRow() {
className="ml-auto" className="ml-auto"
title={ title={
os === "macos" os === "macos"
? "A new Terminal window with these set" ? "A new Terminal window will open with these variables set."
: "Not available on this platform yet" : "Opening a terminal is only supported on macOS."
} }
disabled={caPath == null || os !== "macos"} disabled={caPath == null || os !== "macos"}
isLoading={openTerminal.isPending} isLoading={openTerminal.isPending}
@@ -363,7 +373,7 @@ function TerminalRow() {
Open Terminal Open Terminal
</Button> </Button>
</div> </div>
<p className="mt-1 text-sm text-text-subtle">Proxies one terminal instead of the system</p> <p className="mt-1 text-sm text-text-subtle">Traffic from one terminal will be recorded.</p>
{caPath != null && ( {caPath != null && (
<SettingSnippet <SettingSnippet
className="mt-3" className="mt-3"
@@ -374,7 +384,3 @@ function TerminalRow() {
</div> </div>
); );
} }
function capitalize(s: string): string {
return s.charAt(0).toUpperCase() + s.slice(1);
}
+4 -4
View File
@@ -175,7 +175,7 @@ export function Sidebar() {
onSelect: () => void revealPath(path, fail), onSelect: () => void revealPath(path, fail),
}, },
{ {
label: "Copy path", label: "Copy Path",
leftSlot: <Icon icon="copy" />, leftSlot: <Icon icon="copy" />,
hidden: item.kind !== "folder", hidden: item.kind !== "folder",
onSelect: () => { onSelect: () => {
@@ -203,9 +203,9 @@ export function Sidebar() {
onSelect: async () => { onSelect: async () => {
try { try {
await navigator.clipboard.writeText(copyRowsText(items)); await navigator.clipboard.writeText(copyRowsText(items));
showToast({ message: "Copied value" }); showToast({ message: "Copied to clipboard" });
} catch (err) { } catch (err) {
showToast({ message: `Failed to copy value: ${String(err)}`, color: "danger" }); showToast({ message: `Failed to copy: ${String(err)}`, color: "danger" });
} }
}, },
}, },
@@ -239,7 +239,7 @@ export function Sidebar() {
void applyDecrypt({ void applyDecrypt({
edit: everythingEdit(true), edit: everythingEdit(true),
message: "Decrypting everything", message: "Decrypting everything",
sub: "Except Apple services and apps that pin", sub: "Excluded hosts and apps stay encrypted.",
}), }),
}, },
], ],
@@ -29,11 +29,11 @@ export function TakeOverBanner() {
<span className="truncate"> <span className="truncate">
{!cli ? ( {!cli ? (
// Stopping another app halts its engine without releasing it, so it can't be taken over. // Stopping another app halts its engine without releasing it, so it can't be taken over.
"Another Yaak Proxy window owns the engine. Quit it to use this one." "Quit the other copy to use this one."
) : takeOver.error != null ? ( ) : takeOver.error != null ? (
<span className="text-danger">{takeOver.error.message}</span> <span className="text-danger">{takeOver.error.message}</span>
) : ( ) : (
`pid ${other.pid}, since ${since}. Flows still list here, read from the capture database.` `pid ${other.pid}, since ${since}`
)} )}
</span> </span>
{cli && ( {cli && (
@@ -44,7 +44,7 @@ export function TakeOverBanner() {
isLoading={takeOver.isPending} isLoading={takeOver.isPending}
onClick={() => takeOver.mutate({})} onClick={() => takeOver.mutate({})}
> >
Take over Take Over
</Button> </Button>
)} )}
</div> </div>
+2 -2
View File
@@ -9,8 +9,8 @@ import { syncNavigation, syncSidebarSelection } from "../lib/navigation";
import { syncFlows, syncProxyState } from "../lib/store"; import { syncFlows, syncProxyState } from "../lib/store";
const LOCKED = const LOCKED =
"Traffic is locked: Yaak Proxy stores traffic encrypted and couldn't read its key from the " + "Traffic is locked: Failed to read the encryption key from the keychain: Encryption error: " +
"keychain: Encryption error: User canceled the operation. Allow it access, then retry."; "User canceled the operation";
const STATE: GetProxyStateResponse = { const STATE: GetProxyStateResponse = {
state: "stopped", state: "stopped",
+1 -1
View File
@@ -157,7 +157,7 @@ function mixedScene(): Flow[] {
tunnelled: false, tunnelled: false,
tunnelReason: null, tunnelReason: null,
tlsRejected: "bad_certificate", tlsRejected: "bad_certificate",
error: "Rejected the proxy's certificate", error: "Certificate rejected by the app",
elapsedMs: 6, elapsedMs: 6,
requestSize: 517, requestSize: 517,
responseSize: 0, responseSize: 0,
+1 -1
View File
@@ -46,7 +46,7 @@ export function useTrustCertificate() {
showToast({ showToast({
id: "decrypt", id: "decrypt",
color: "danger", color: "danger",
message: `Certificate not trusted: ${err.message}`, message: `Failed to trust certificate: ${err.message}`,
}), }),
}); });
} }
+1 -1
View File
@@ -231,7 +231,7 @@ describe("flowBodyQueryOptions", () => {
describe("a body that isn't there", () => { describe("a body that isn't there", () => {
test("a WebSocket's note reaches the viewer, which points at its messages", () => { test("a WebSocket's note reaches the viewer, which points at its messages", () => {
const note = "A WebSocket has no body after its 101 response. Its frames are its messages."; const note = "A WebSocket has no body";
const body = viewerBody({ data: "", truncated: false, storedSize: 0, note }); const body = viewerBody({ data: "", truncated: false, storedSize: 0, note });
expect(body.note).toBe(note); expect(body.note).toBe(note);
expect(emptyBodyText("response", body.note)).toBe("No body. See Messages."); expect(emptyBodyText("response", body.note)).toBe("No body. See Messages.");
+1 -1
View File
@@ -194,7 +194,7 @@ describe("the sidebar menu", () => {
expect(decryptMenuAction([], [site])?.label).toBe("Decrypt *.google.com"); expect(decryptMenuAction([], [site])?.label).toBe("Decrypt *.google.com");
expect(decryptMenuAction([], [dia])?.label).toBe("Decrypt Dia"); expect(decryptMenuAction([], [dia])?.label).toBe("Decrypt Dia");
expect(decryptMenuAction([], [site, { kind: "site", site: "x.com" }])?.label).toBe( expect(decryptMenuAction([], [site, { kind: "site", site: "x.com" }])?.label).toBe(
"Decrypt 2 Sites", "Decrypt 2 Domains",
); );
expect(decryptMenuAction([], [dia, a, site])?.label).toBe("Decrypt 3 Selected"); expect(decryptMenuAction([], [dia, a, site])?.label).toBe("Decrypt 3 Selected");
}); });
+5 -5
View File
@@ -411,8 +411,8 @@ export function subjectName(subject: DecryptSubject): string {
} }
/** /**
* Several subjects named together: one by its name, several of a kind counted, `3 Sites` in a * Several subjects named together: one by its name, several of a kind counted, `3 Domains` in a
* menu and `3 sites` in a toast, and mixed kinds as `3 Selected` * menu and `3 domains` in a toast, and mixed kinds as `3 Selected`
*/ */
function subjectsName(subjects: DecryptSubject[]): { title: string; text: string } { function subjectsName(subjects: DecryptSubject[]): { title: string; text: string } {
const [first] = subjects; const [first] = subjects;
@@ -423,7 +423,7 @@ function subjectsName(subjects: DecryptSubject[]): { title: string; text: string
const kinds = new Set(subjects.map((s) => s.kind)); const kinds = new Set(subjects.map((s) => s.kind));
const n = subjects.length; const n = subjects.length;
if (kinds.size > 1) return { title: `${n} Selected`, text: `${n} selected` }; if (kinds.size > 1) return { title: `${n} Selected`, text: `${n} selected` };
const noun = first?.kind === "app" ? "Apps" : first?.kind === "site" ? "Sites" : "Hosts"; const noun = first?.kind === "app" ? "Apps" : first?.kind === "site" ? "Domains" : "Hosts";
return { title: `${n} ${noun}`, text: `${n} ${noun.toLowerCase()}` }; return { title: `${n} ${noun}`, text: `${n} ${noun.toLowerCase()}` };
} }
@@ -473,8 +473,8 @@ export function decryptFilterOptions(seen: SeenTargets | null | undefined): Filt
replaceValueOnly: true, replaceValueOnly: true,
negation: { prefix: "!", info: "Exclude" }, negation: { prefix: "!", info: "Exclude" },
fields: [ fields: [
{ name: "host", values: seen?.hosts ?? [], info: "Host or *.site" }, { name: "host", values: seen?.hosts ?? [], info: "Host or *.domain" },
{ name: "app", values: [...apps], info: "App name or bundle id" }, { name: "app", values: [...apps], info: "App name or bundle ID" },
], ],
}; };
} }
+2 -2
View File
@@ -106,10 +106,10 @@ describe("the toast after a choice", () => {
test("offers Reconnect for the entry when open connections stay encrypted", () => { test("offers Reconnect for the entry when open connections stay encrypted", () => {
expect(choiceToast(choice, 3)).toEqual({ expect(choiceToast(choice, 3)).toEqual({
message: "Decrypting www.google.com", message: "Decrypting www.google.com",
sub: "3 open connections stay encrypted", sub: "3 open connections stay encrypted.",
reconnect: ["host:www.google.com"], reconnect: ["host:www.google.com"],
}); });
expect(choiceToast(choice, 1).sub).toBe("1 open connection stays encrypted"); expect(choiceToast(choice, 1).sub).toBe("1 open connection stays encrypted.");
}); });
test("Reconnect for an exclusion taken off names the exclusion, as the engine reads it", () => { test("Reconnect for an exclusion taken off names the exclusion, as the engine reads it", () => {
+1 -1
View File
@@ -81,7 +81,7 @@ export function choiceToast(choice: DecryptChoice, stillEncrypted: number): Choi
const reconnect = reconnectEntries(choice.edit); const reconnect = reconnectEntries(choice.edit);
return { return {
message: choice.message, message: choice.message,
sub: `${n} open ${stillEncrypted === 1 ? "connection stays" : "connections stay"} encrypted`, sub: `${n} open ${stillEncrypted === 1 ? "connection stays" : "connections stay"} encrypted.`,
reconnect, reconnect,
}; };
} }
+33 -31
View File
@@ -68,7 +68,9 @@ describe("connectionOf", () => {
describe("the card of an encrypted connection", () => { describe("the card of an encrypted connection", () => {
test("nothing decrypted yet: the host, the app and everything, each with what it writes", () => { test("nothing decrypted yet: the host, the app and everything, each with what it writes", () => {
const c = card(); const c = card();
expect(lineText(c.why)).toBe("Nothing is decrypted yet"); expect(lineText(c.why)).toBe(
"**Encrypted Content:** Enable decryption to see requests to `www.google.com`",
);
expect(summary(c)).toEqual([ expect(summary(c)).toEqual([
[ [
"Decrypt www.google.com", "Decrypt www.google.com",
@@ -80,11 +82,12 @@ describe("the card of an encrypted connection", () => {
]); ]);
// Nothing under the buttons while the certificate is trusted // Nothing under the buttons while the certificate is trusted
expect(c.foot).toEqual([]); expect(c.foot).toEqual([]);
expect(c.fact).toBeNull(); // The info button keeps its general line
expect(c.tooltip).toBeNull();
}); });
test("asks for the password once while the certificate isn't trusted, where it can be", () => { test("asks for the password once while the certificate isn't trusted, where it can be", () => {
expect(card({ trusted: false }).foot).toEqual(["Asks for your password once"]); expect(card({ trusted: false }).foot).toEqual(["macOS will ask for a password once."]);
expect(card({ trusted: false, canTrust: false }).foot).toEqual([]); expect(card({ trusted: false, canTrust: false }).foot).toEqual([]);
}); });
@@ -140,10 +143,7 @@ describe("the card of an encrypted connection", () => {
tunnelExcludedBuiltIn: true, tunnelExcludedBuiltIn: true,
}, },
}); });
expect([lineText(pinned.why), pinned.whySub]).toEqual([ expect(lineText(pinned.why)).toBe("Apps that pin certificates are excluded by default.");
"Excluded by default",
"pins its certificate",
]);
expect(summary(pinned)).toEqual([ expect(summary(pinned)).toEqual([
[ [
"Decrypt my.1password.com", "Decrypt my.1password.com",
@@ -152,7 +152,7 @@ describe("the card of an encrypted connection", () => {
], ],
]); ]);
expect(pinned.actions[0]?.dashed).toBe(true); expect(pinned.actions[0]?.dashed).toBe(true);
expect(pinned.foot).toEqual(["1Password may refuse it"]); expect(pinned.foot).toEqual(["1Password may reject the connection."]);
const apple = card({ const apple = card({
flow: { flow: {
@@ -165,8 +165,8 @@ describe("the card of an encrypted connection", () => {
tunnelExcludedBuiltIn: true, tunnelExcludedBuiltIn: true,
}, },
}); });
expect(apple.whySub).toBe("Apple service"); expect(lineText(apple.why)).toBe("Apple services are excluded by default.");
expect(apple.foot).toEqual(["macOS may refuse it"]); expect(apple.foot).toEqual(["macOS may reject the connection."]);
}); });
test("excluded by the person: names it, and Stop Excluding when that alone decrypts it", () => { test("excluded by the person: names it, and Stop Excluding when that alone decrypts it", () => {
@@ -175,7 +175,7 @@ describe("the card of an encrypted connection", () => {
list, list,
flow: { ...tunnel, tunnelReason: "excluded", tunnelExcludedBy: "!host:*.google.com" }, flow: { ...tunnel, tunnelReason: "excluded", tunnelExcludedBy: "!host:*.google.com" },
}); });
expect(lineText(c.why)).toBe("Excluded by `!host:*.google.com`"); expect(lineText(c.why)).toBe("This connection is excluded by `!host:*.google.com`.");
// Nothing added wins over the person's own exclusion, so taking it off is the way in // Nothing added wins over the person's own exclusion, so taking it off is the way in
expect(summary(c)).toEqual([ expect(summary(c)).toEqual([
[ [
@@ -208,9 +208,8 @@ describe("the card of an encrypted connection", () => {
test("once the list decrypts it: opened before, the opposite, and Reconnect while open", () => { test("once the list decrypts it: opened before, the opposite, and Reconnect while open", () => {
const list = [...user("host:www.google.com"), ...BUILT_INS]; const list = [...user("host:www.google.com"), ...BUILT_INS];
const closed = card({ list }); const closed = card({ list });
expect(lineText(closed.why)).toBe("Not on the decrypt list"); expect(lineText(closed.why)).toBe(
expect(closed.fact && lineText(closed.fact)).toBe( "This connection was opened before `www.google.com` was decrypted.",
"Opened before `www.google.com` was decrypted",
); );
expect(summary(closed)).toEqual([ expect(summary(closed)).toEqual([
[ [
@@ -225,8 +224,8 @@ describe("the card of an encrypted connection", () => {
expect(open.actions.map((a) => a.kind)).toEqual(["reconnect", "stop"]); expect(open.actions.map((a) => a.kind)).toEqual(["reconnect", "stop"]);
const everything = card({ list: [...user("*"), ...BUILT_INS] }); const everything = card({ list: [...user("*"), ...BUILT_INS] });
expect(everything.fact && lineText(everything.fact)).toBe( expect(lineText(everything.why)).toBe(
"Opened before everything was decrypted", "This connection was opened before everything was decrypted.",
); );
expect(everything.actions[0]?.label).toBe("Stop Decrypting Everything"); expect(everything.actions[0]?.label).toBe("Stop Decrypting Everything");
}); });
@@ -245,22 +244,25 @@ describe("the card of a refused handshake", () => {
}; };
const listed = [...user("*"), ...BUILT_INS]; const listed = [...user("*"), ...BUILT_INS];
test("an app: likely pinned, Stop Decrypting first, Try Again while passed through", () => { test("an app: likely pinned, Stop Decrypting first, Retry while passed through", () => {
const c = card({ flow: refused, list: listed, passthrough: ["sync.acme.dev:443"] }); const c = card({ flow: refused, list: listed, passthrough: ["sync.acme.dev:443"] });
expect(c.rejected).toBe(true); expect(c.rejected).toBe(true);
expect([lineText(c.why), c.whySub]).toEqual(["Acme rejected the certificate", "likely pinned"]); expect([lineText(c.why), c.tooltip]).toEqual([
"Acme rejected the certificate.",
"Acme likely pins its certificate, so it cannot be decrypted.",
]);
expect(summary(c)).toEqual([ expect(summary(c)).toEqual([
["Stop Decrypting Acme", "!app:Acme", { add: ["!app:Acme"], remove: [] }], ["Stop Decrypting Acme", "!app:Acme", { add: ["!app:Acme"], remove: [] }],
["Try Again", "next connection", null], ["Retry", "next connection", null],
]); ]);
expect(c.actions[0]?.primary).toBe(true); expect(c.actions[0]?.primary).toBe(true);
expect(c.foot).toEqual(["Passed through until relaunch"]); expect(c.foot).toEqual(["This host stays encrypted until Yaak Proxy is relaunched."]);
}); });
test("once no longer passed through, Try Again goes and the foot says so", () => { test("once no longer passed through, Retry goes and the foot says so", () => {
const c = card({ flow: refused, list: listed, passthrough: [] }); const c = card({ flow: refused, list: listed, passthrough: [] });
expect(c.actions.map((a) => a.kind)).toEqual(["stop"]); expect(c.actions.map((a) => a.kind)).toEqual(["stop"]);
expect(c.foot).toEqual(["No longer passed through"]); expect(c.foot).toEqual(["The next connection will be decrypted."]);
}); });
test("a program: its own trust store, Copy Environment first", () => { test("a program: its own trust store, Copy Environment first", () => {
@@ -272,16 +274,16 @@ describe("the card of a refused handshake", () => {
clientPath: "/opt/homebrew/bin/python3", clientPath: "/opt/homebrew/bin/python3",
}; };
const c = card({ flow: python, list: listed, passthrough: ["pypi.org:443"] }); const c = card({ flow: python, list: listed, passthrough: ["pypi.org:443"] });
expect([lineText(c.why), c.whySub]).toEqual([ expect([lineText(c.why), c.tooltip]).toEqual([
"python3 rejected the certificate", "python3 rejected the certificate.",
"uses its own trust store", "python3 uses its own trust store, so it does not trust the certificate.",
]); ]);
expect(c.actions.map((a) => [a.kind, a.label])).toEqual([ expect(c.actions.map((a) => [a.kind, a.label])).toEqual([
["copy_environment", "Copy Environment"], ["copy_environment", "Copy Environment"],
["stop", "Stop Decrypting python3"], ["stop", "Stop Decrypting python3"],
["try_again", "Try Again"], ["try_again", "Retry"],
]); ]);
expect(c.foot).toEqual(["Then rerun python3"]); expect(c.foot).toEqual(["Paste it into a terminal, then rerun python3."]);
}); });
test("an unknown client: the host", () => { test("an unknown client: the host", () => {
@@ -289,7 +291,7 @@ describe("the card of a refused handshake", () => {
flow: { ...refused, clientName: null, clientBundleId: null, clientPath: null }, flow: { ...refused, clientName: null, clientBundleId: null, clientPath: null },
list: listed, list: listed,
}); });
expect(lineText(c.why)).toBe("A client rejected the certificate"); expect(lineText(c.why)).toBe("An app rejected the certificate.");
expect(summary(c)[0]).toEqual([ expect(summary(c)[0]).toEqual([
"Stop Decrypting sync.acme.dev", "Stop Decrypting sync.acme.dev",
"!host:sync.acme.dev", "!host:sync.acme.dev",
@@ -304,9 +306,9 @@ describe("the card of a refused handshake", () => {
trusted: false, trusted: false,
passthrough: ["sync.acme.dev:443"], passthrough: ["sync.acme.dev:443"],
}); });
expect(lineText(c.why)).toBe("Certificate not trusted"); expect(lineText(c.why)).toBe("The certificate is not trusted.");
expect(c.actions.map((a) => a.kind)).toEqual(["trust", "stop"]); expect(c.actions.map((a) => a.kind)).toEqual(["trust", "stop"]);
expect(c.foot).toEqual(["Passed through until trusted"]); expect(c.foot).toEqual(["This host stays encrypted until the certificate is trusted."]);
}); });
test("a tunnel after a refusal names the client that refused", () => { test("a tunnel after a refusal names the client that refused", () => {
@@ -320,7 +322,7 @@ describe("the card of a refused handshake", () => {
list: listed, list: listed,
rejectedBy: { clientName: "Acme", clientBundleId: "dev.acme", clientPath: null }, rejectedBy: { clientName: "Acme", clientBundleId: "dev.acme", clientPath: null },
}); });
expect(lineText(c.why)).toBe("Acme rejected the certificate for this host"); expect(lineText(c.why)).toBe("Acme rejected the certificate for this host.");
// Stop Decrypting acts on this connection, so on Dia's own // Stop Decrypting acts on this connection, so on Dia's own
expect(c.actions[0]?.hint).toBe("!app:Dia"); expect(c.actions[0]?.hint).toBe("!app:Dia");
}); });
+44 -55
View File
@@ -95,12 +95,10 @@ export function lineText(line: Line): string {
export interface EncryptedCardModel { export interface EncryptedCardModel {
rejected: boolean; rejected: boolean;
/** Why the connection is encrypted, in one line */ /** Why the connection is encrypted, in one sentence */
why: Line; why: Line;
/** Quieter, after it */ /** What the info button beside it says, in place of the general line; null for that line */
whySub: string | null; tooltip: string | null;
/** A fact about the connection since the list changed: when it opened */
fact: Line | null;
actions: CardAction[]; actions: CardAction[];
/** Small lines under the buttons */ /** Small lines under the buttons */
foot: string[]; foot: string[];
@@ -155,7 +153,6 @@ function tunnelCard({ flow, list, trusted, canTrust }: EncryptedCardInput): Encr
const c = connectionOf(flow); const c = connectionOf(flow);
const q = queryOf(c); const q = queryOf(c);
const decryptsNow = decrypts(list, q); const decryptsNow = decrypts(list, q);
const { why, whySub } = whyLine(flow, list, c, decryptsNow);
const foot: string[] = []; const foot: string[] = [];
if (decryptsNow) { if (decryptsNow) {
@@ -172,9 +169,8 @@ function tunnelCard({ flow, list, trusted, canTrust }: EncryptedCardInput): Encr
first === EVERYTHING ? "everything" : { code: first == null ? c.label : entrySubject(first) }; first === EVERYTHING ? "everything" : { code: first == null ? c.label : entrySubject(first) };
return { return {
rejected: false, rejected: false,
why, why: ["This connection was opened before ", what, " was decrypted."],
whySub, tooltip: null,
fact: ["Opened before ", what, " was decrypted"],
actions, actions,
foot: [], foot: [],
}; };
@@ -193,8 +189,8 @@ function tunnelCard({ flow, list, trusted, canTrust }: EncryptedCardInput): Encr
foot.length = 0; foot.length = 0;
foot.push( foot.push(
builtInBlock.kind === "apple" || c.app == null builtInBlock.kind === "apple" || c.app == null
? "macOS may refuse it" ? "macOS may reject the connection."
: `${c.app.name} may refuse it`, : `${c.app.name} may reject the connection.`,
); );
} }
} else { } else {
@@ -209,7 +205,7 @@ function tunnelCard({ flow, list, trusted, canTrust }: EncryptedCardInput): Encr
label: "Decrypt Everything", label: "Decrypt Everything",
hint: EVERYTHING, hint: EVERYTHING,
edit: { add: [EVERYTHING], remove: [] }, edit: { add: [EVERYTHING], remove: [] },
toast: { message: "Decrypting everything", sub: "Except Apple services and apps that pin" }, toast: { message: "Decrypting everything", sub: "Excluded hosts and apps stay encrypted." },
icon: "everything", icon: "everything",
}); });
} }
@@ -237,48 +233,41 @@ function tunnelCard({ flow, list, trusted, canTrust }: EncryptedCardInput): Encr
} }
} }
} }
if (actions.length > 0 && canTrust && trusted === false) foot.push("Asks for your password once"); if (actions.length > 0 && canTrust && trusted === false) {
foot.push("macOS will ask for a password once.");
}
return { return {
rejected: false, rejected: false,
why, why: whyLine(flow, list, c),
whySub, tooltip: null,
fact: null,
actions, actions,
foot: actions.length === 0 ? [] : foot, foot: actions.length === 0 ? [] : foot,
}; };
} }
/** /**
* The why line, from the reason the engine recorded as the tunnel opened. One that nothing on * The why line of a connection the list does not decrypt, from the reason the engine recorded
* the list covers names its host; once the list covers it, it opened before that. * as the tunnel opened: what excluded it, or, with nothing to exclude it, its host.
*/ */
function whyLine( function whyLine(flow: EncryptedCardInput["flow"], list: DecryptListEntry[], c: Connection): Line {
flow: EncryptedCardInput["flow"],
list: DecryptListEntry[],
c: Connection,
decryptsNow: boolean,
): { why: Line; whySub: string | null } {
if (flow.tunnelReason === "excluded" && flow.tunnelExcludedBy != null) { if (flow.tunnelReason === "excluded" && flow.tunnelExcludedBy != null) {
if (flow.tunnelExcludedBuiltIn) { if (flow.tunnelExcludedBuiltIn) {
const kind = list.find((e) => e.entry === flow.tunnelExcludedBy)?.kind; const kind = list.find((e) => e.entry === flow.tunnelExcludedBy)?.kind;
const whySub = return [
kind === "apple" ? "Apple service" : kind === "pinned" ? "pins its certificate" : null; kind === "apple"
return { why: ["Excluded by default"], whySub }; ? "Apple services are excluded by default."
: kind === "pinned"
? "Apps that pin certificates are excluded by default."
: "This connection is excluded by default.",
];
} }
return { why: ["Excluded by ", { code: flow.tunnelExcludedBy }], whySub: null }; return ["This connection is excluded by ", { code: flow.tunnelExcludedBy }, "."];
} }
return { return [
why: !decryptsAnything(list) { bold: "Encrypted Content:" },
? ["Nothing is decrypted yet"] " Enable decryption to see requests to ",
: decryptsNow { code: c.label },
? ["Not on the decrypt list"] ];
: [
{ bold: "Encrypted Content:" },
" Enable decryption to see requests to ",
{ code: c.label },
],
whySub: null,
};
} }
function decryptSubject( function decryptSubject(
@@ -370,20 +359,19 @@ function rejectedCard(input: EncryptedCardInput): EncryptedCardModel {
const tryAgain: CardAction | null = const tryAgain: CardAction | null =
passedThrough === false passedThrough === false
? null ? null
: { kind: "try_again", label: "Try Again", hint: "next connection", icon: "refresh" }; : { kind: "try_again", label: "Retry", hint: "next connection", icon: "refresh" };
const until = passedThrough === false ? "No longer passed through" : null; const until = passedThrough === false ? "The next connection will be decrypted." : null;
if (canTrust && trusted === false && decryptsAnything(list)) { if (canTrust && trusted === false && decryptsAnything(list)) {
return { return {
rejected: true, rejected: true,
why: ["Certificate not trusted"], why: ["The certificate is not trusted."],
whySub: null, tooltip: null,
fact: null,
actions: [ actions: [
{ kind: "trust", label: "Trust Certificate", hint: null, primary: true, icon: "shield" }, { kind: "trust", label: "Trust Certificate", hint: null, primary: true, icon: "shield" },
...(stop == null ? [] : [stop]), ...(stop == null ? [] : [stop]),
], ],
foot: [until ?? "Passed through until trusted"], foot: [until ?? "This host stays encrypted until the certificate is trusted."],
}; };
} }
@@ -391,9 +379,8 @@ function rejectedCard(input: EncryptedCardInput): EncryptedCardModel {
if (refuser.kind === "program") { if (refuser.kind === "program") {
return { return {
rejected: true, rejected: true,
why: [`${refuser.name} rejected the certificate${forHost}`], why: [`${refuser.name} rejected the certificate${forHost}.`],
whySub: "uses its own trust store", tooltip: `${refuser.name} uses its own trust store, so it does not trust the certificate.`,
fact: null,
actions: [ actions: [
{ {
kind: "copy_environment", kind: "copy_environment",
@@ -405,22 +392,24 @@ function rejectedCard(input: EncryptedCardInput): EncryptedCardModel {
...(stop == null ? [] : [stop]), ...(stop == null ? [] : [stop]),
...(tryAgain == null ? [] : [tryAgain]), ...(tryAgain == null ? [] : [tryAgain]),
], ],
foot: until == null ? [`Then rerun ${refuser.name}`] : [until], foot: until == null ? [`Paste it into a terminal, then rerun ${refuser.name}.`] : [until],
}; };
} }
return { return {
rejected: true, rejected: true,
why: [ why: [
refuser.kind === "app" refuser.kind === "app"
? `${refuser.name} rejected the certificate${forHost}` ? `${refuser.name} rejected the certificate${forHost}.`
: "A client rejected the certificate", : "An app rejected the certificate.",
], ],
whySub: refuser.kind === "app" ? "likely pinned" : null, tooltip:
fact: null, refuser.kind === "app"
? `${refuser.name} likely pins its certificate, so it cannot be decrypted.`
: null,
actions: [ actions: [
...(stop == null ? [] : [{ ...stop, primary: true }]), ...(stop == null ? [] : [{ ...stop, primary: true }]),
...(tryAgain == null ? [] : [tryAgain]), ...(tryAgain == null ? [] : [tryAgain]),
], ],
foot: [until ?? "Passed through until relaunch"], foot: [until ?? "This host stays encrypted until Yaak Proxy is relaunched."],
}; };
} }
+7 -7
View File
@@ -33,8 +33,8 @@ export function useSaveFlowBody(): (flow: Flow, which: FlowBody) => Promise<void
if (path == null) return; if (path == null) return;
const saved = await rpc("save_flow_body", { flowId: flow.id, which, path }); const saved = await rpc("save_flow_body", { flowId: flow.id, which, path });
const notes = [ const notes = [
saved.raw ? "as it was on the wire, since it failed to decode" : null, saved.raw ? "as received, since it failed to decode" : null,
saved.truncated ? "cut at the capture limit" : null, saved.truncated ? "truncated at the capture limit" : null,
].filter((n) => n != null); ].filter((n) => n != null);
showToast({ showToast({
message: `Saved ${formatSize(saved.size)}${notes.length > 0 ? `, ${notes.join(", ")}` : ""}`, message: `Saved ${formatSize(saved.size)}${notes.length > 0 ? `, ${notes.join(", ")}` : ""}`,
@@ -135,9 +135,9 @@ export function useFlowActions(): (flow: Flow) => DropdownItem[] {
return; return;
} }
await navigator.clipboard.writeText(value); await navigator.clipboard.writeText(value);
showToast({ message: `Copied ${label}` }); showToast({ message: "Copied to clipboard" });
} catch (err) { } catch (err) {
showToast({ message: `Failed to copy ${label}: ${String(err)}`, color: "danger" }); showToast({ message: `Failed to copy: ${String(err)}`, color: "danger" });
} }
}; };
const noResponse = (flow.error != null || flow.cancelled) && flow.status == null; const noResponse = (flow.error != null || flow.cancelled) && flow.status == null;
@@ -173,7 +173,7 @@ export function useFlowActions(): (flow: Flow) => DropdownItem[] {
// A command that sends other bytes than the app did would mislead. // A command that sends other bytes than the app did would mislead.
if (body == null) { if (body == null) {
showToast({ showToast({
message: "Body is binary or truncated; export HAR instead", message: "The body is binary or truncated. Export as HAR instead.",
color: "info", color: "info",
}); });
return; return;
@@ -209,12 +209,12 @@ export function useFlowActions(): (flow: Flow) => DropdownItem[] {
onSelect: () => saveBody(flow, "response"), onSelect: () => saveBody(flow, "response"),
}, },
{ {
label: "Export HAR", label: "Export as HAR",
icon: "download", icon: "download",
onSelect: async () => { onSelect: async () => {
try { try {
const written = await exportHar({ ids: [flow.id] }, "flow.har"); const written = await exportHar({ ids: [flow.id] }, "flow.har");
if (written != null) showToast({ message: "Exported the flow as HAR" }); if (written != null) showToast({ message: "Exported 1 flow as HAR" });
} catch (err) { } catch (err) {
showToast({ message: `Failed to export HAR: ${String(err)}`, color: "danger" }); showToast({ message: `Failed to export HAR: ${String(err)}`, color: "danger" });
} }
+1 -1
View File
@@ -225,7 +225,7 @@ describe("flowResponseToView", () => {
test("says when the client gave up before a response", () => { test("says when the client gave up before a response", () => {
const view = flowResponseToView({ ...flow, status: null, elapsedMs: null, cancelled: true }); const view = flowResponseToView({ ...flow, status: null, elapsedMs: null, cancelled: true });
expect(view?.error).toBe("Cancelled by the client"); expect(view?.error).toBe("Cancelled by the app");
}); });
}); });
+1 -1
View File
@@ -119,7 +119,7 @@ function responseView(flow: Flow, body: FlowBodyData | null, ended: boolean): Ht
elapsedMs: flow.elapsedMs ?? 0, elapsedMs: flow.elapsedMs ?? 0,
elapsedHeadersMs: flow.elapsedHeadersMs ?? 0, elapsedHeadersMs: flow.elapsedHeadersMs ?? 0,
elapsedDnsMs: 0, elapsedDnsMs: 0,
error: flow.error ?? (flow.cancelled && flow.status == null ? "Cancelled by the client" : null), error: flow.error ?? (flow.cancelled && flow.status == null ? "Cancelled by the app" : null),
remoteAddr: flow.remoteAddr, remoteAddr: flow.remoteAddr,
version: flow.upstreamHttpVersion ?? flow.clientHttpVersion, version: flow.upstreamHttpVersion ?? flow.clientHttpVersion,
cookieHeaders: { cookieHeaders: {
+2 -2
View File
@@ -14,8 +14,8 @@ describe("lockedLine", () => {
test("gives any other reason without the title's words", () => { test("gives any other reason without the title's words", () => {
const lock = { const lock = {
keychain: false, keychain: false,
message: "Traffic is locked: the proxy is already running in the yaak CLI (pid 4).", message: "Traffic is locked: the proxy is already running in the CLI (pid 4).",
}; };
expect(lockedLine(lock, "macos")).toBe("The proxy is already running in the yaak CLI (pid 4)."); expect(lockedLine(lock, "macos")).toBe("The proxy is already running in the CLI (pid 4).");
}); });
}); });
+1 -1
View File
@@ -271,7 +271,7 @@ describe("syncSidebarSelection", () => {
test("All traffic is exclusive either way round", () => { test("All traffic is exclusive either way round", () => {
const { store, selected, idOf, choose } = sidebar(); const { store, selected, idOf, choose } = sidebar();
const [all, github] = [idOf("All traffic"), idOf("github.com")]; const [all, github] = [idOf("All Traffic"), idOf("github.com")];
choose([github]); choose([github]);
choose([github, all]); choose([github, all]);
expect(store.get(selected)).toEqual([all]); expect(store.get(selected)).toEqual([all]);
+2 -2
View File
@@ -20,7 +20,7 @@ describe("opener", () => {
await revealPath("/data/app.yaak.proxy", report); await revealPath("/data/app.yaak.proxy", report);
expect(opener.revealItemInDir).toHaveBeenCalledWith("/data/app.yaak.proxy"); expect(opener.revealItemInDir).toHaveBeenCalledWith("/data/app.yaak.proxy");
expect(report).toHaveBeenCalledWith( expect(report).toHaveBeenCalledWith(
"Failed to show /data/app.yaak.proxy: not allowed: opener.reveal_item_in_dir", "Failed to reveal /data/app.yaak.proxy: not allowed: opener.reveal_item_in_dir",
); );
}); });
@@ -43,6 +43,6 @@ describe("opener", () => {
test("names the file manager by platform", () => { test("names the file manager by platform", () => {
expect(revealLabel("macos")).toBe("Reveal in Finder"); expect(revealLabel("macos")).toBe("Reveal in Finder");
expect(revealLabel("windows")).toBe("Show in Explorer"); expect(revealLabel("windows")).toBe("Show in Explorer");
expect(revealLabel("linux")).toBe("Show in Folder"); expect(revealLabel("linux")).toBe("Show in File Manager");
}); });
}); });
+2 -2
View File
@@ -8,7 +8,7 @@ export async function revealPath(path: string, report: ReportFailure): Promise<v
try { try {
await revealItemInDir(path); await revealItemInDir(path);
} catch (err) { } catch (err) {
report(`Failed to show ${path}: ${String(err)}`); report(`Failed to reveal ${path}: ${String(err)}`);
} }
} }
@@ -25,5 +25,5 @@ export async function openLink(url: string, report: ReportFailure): Promise<void
export function revealLabel(os: string): string { export function revealLabel(os: string): string {
if (os === "macos") return "Reveal in Finder"; if (os === "macos") return "Reveal in Finder";
if (os === "windows") return "Show in Explorer"; if (os === "windows") return "Show in Explorer";
return "Show in Folder"; return "Show in File Manager";
} }
+1 -1
View File
@@ -41,7 +41,7 @@ export async function loadSeenValues(paths: string[]): Promise<Record<string, st
const PROBLEM_LABELS: Record<QueryDiagnostic["kind"], string> = { const PROBLEM_LABELS: Record<QueryDiagnostic["kind"], string> = {
unknownField: "Unknown field", unknownField: "Unknown field",
invalidValue: "Invalid value", invalidValue: "Invalid value",
notYet: "Not yet", notYet: "Not supported",
}; };
/** The terms of a search the backend cannot run, as the box underlines them */ /** The terms of a search the backend cannot run, as the box underlines them */
+2 -2
View File
@@ -50,7 +50,7 @@ describe("buildSidebarTree", () => {
test("groups apps, programs by folder, sites and System", () => { test("groups apps, programs by folder, sites and System", () => {
expect((root.children ?? []).flatMap((c) => shape(c))).toEqual([ expect((root.children ?? []).flatMap((c) => shape(c))).toEqual([
"All traffic 40", "All Traffic 40",
"Apps 3", "Apps 3",
" node (fieldwork) 2", " node (fieldwork) 2",
" github.com 2", " github.com 2",
@@ -338,7 +338,7 @@ describe("saved searches", () => {
]; ];
const root = buildSidebarTree(flows, 4, saved); const root = buildSidebarTree(flows, 4, saved);
expect((root.children ?? []).slice(0, 2).flatMap((c) => shape(c))).toEqual([ expect((root.children ?? []).slice(0, 2).flatMap((c) => shape(c))).toEqual([
"All traffic 4", "All Traffic 4",
"Saved 0", "Saved 0",
" Errors 12", " Errors 12",
" Stripe 0", " Stripe 0",
+1 -1
View File
@@ -347,7 +347,7 @@ export function buildSidebarTree(
for (const site of domains.children.values()) domains.item.count += site.children.size; for (const site of domains.children.values()) domains.item.count += site.children.size;
root.children?.push({ root.children?.push({
item: { id: ALL_ID, kind: "all", label: "All traffic", count: total, query: "" }, item: { id: ALL_ID, kind: "all", label: "All Traffic", count: total, query: "" },
parent: root, parent: root,
depth: 1, depth: 1,
draggable: false, draggable: false,
+23 -18
View File
@@ -14,12 +14,12 @@ describe("systemProxySwitch", () => {
test("says whether this Mac's traffic is routed now", () => { test("says whether this Mac's traffic is routed now", () => {
expect(systemProxySwitch({ ...off, routing: true })).toEqual({ expect(systemProxySwitch({ ...off, routing: true })).toEqual({
checked: true, checked: true,
description: "Routing this Mac's web traffic through Yaak Proxy", description: "Traffic from every app will be recorded.",
failed: false, failed: false,
}); });
expect(systemProxySwitch(off)).toEqual({ expect(systemProxySwitch(off)).toEqual({
checked: false, checked: false,
description: "Not routing. Only apps pointed at port 9090 are captured", description: "Only traffic sent to port 9090 will be recorded.",
failed: false, failed: false,
}); });
}); });
@@ -27,7 +27,7 @@ describe("systemProxySwitch", () => {
test("shows the new value while the password prompt is up", () => { test("shows the new value while the password prompt is up", () => {
expect(systemProxySwitch({ ...off, pending: true })).toEqual({ expect(systemProxySwitch({ ...off, pending: true })).toEqual({
checked: true, checked: true,
description: "Waiting for your password", description: "Waiting for macOS",
failed: false, failed: false,
}); });
}); });
@@ -41,9 +41,9 @@ describe("systemProxySwitch", () => {
}); });
test("snaps back on a cancelled prompt and says so", () => { test("snaps back on a cancelled prompt and says so", () => {
expect(systemProxySwitch({ ...off, error: "Cancelled in the password dialog" })).toEqual({ expect(systemProxySwitch({ ...off, error: "The password prompt was cancelled." })).toEqual({
checked: false, checked: false,
description: "Cancelled in the password prompt", description: "The password prompt was cancelled.",
failed: true, failed: true,
}); });
expect(systemProxySwitch({ ...off, error: "networksetup failed" }).description).toBe( expect(systemProxySwitch({ ...off, error: "networksetup failed" }).description).toBe(
@@ -53,27 +53,32 @@ describe("systemProxySwitch", () => {
}); });
describe("helperRow", () => { describe("helperRow", () => {
test("offers each next step, and says which way changes go", () => { test("offers each next step, and says what the helper is for", () => {
expect(helperRow("not_registered", "admin_prompt")).toMatchObject({ expect(helperRow("not_registered", "admin_prompt")).toMatchObject({
title: "Helper: not installed", description: "The helper is not installed.",
action: "install", action: "install",
}); });
expect(helperRow("requires_approval", "admin_prompt")).toMatchObject({ expect(helperRow("requires_approval", "admin_prompt")).toMatchObject({
title: "Helper: awaiting approval", description: "The helper is awaiting approval.",
action: "approve", action: "approve",
}); });
const enabled = helperRow("enabled", "helper"); const enabled = helperRow("enabled", "helper");
expect(enabled).toMatchObject({ title: "Helper: enabled", action: "unregister" }); expect(enabled).toMatchObject({
expect(enabled.description).toMatch(/No password/); description: "The helper is installed.",
action: "unregister",
});
expect(enabled.tooltip).toMatch(/without a password prompt/);
}); });
test("says when the password prompt still has settings to put back", () => { test("says when the password prompt still has settings to put back", () => {
expect(helperRow("enabled", "admin_prompt").description).toMatch(/password prompt/); expect(helperRow("enabled", "admin_prompt").tooltip).toBe(
"One more password prompt is needed to restore the system proxy settings.",
);
}); });
test("offers to install when macOS can't find it, rather than blaming a dev build", () => { test("offers to install when macOS can't find it, rather than blaming a dev build", () => {
expect(helperRow("not_found", "admin_prompt")).toMatchObject({ expect(helperRow("not_found", "admin_prompt")).toMatchObject({
title: "Helper: not found by macOS", description: "macOS could not find the helper.",
action: "install", action: "install",
}); });
}); });
@@ -89,13 +94,13 @@ describe("helperRow", () => {
for (const state of states) { for (const state of states) {
for (const backend of ["helper", "admin_prompt"] as const) { for (const backend of ["helper", "admin_prompt"] as const) {
const row = helperRow(state, backend); const row = helperRow(state, backend);
expect(row.title.length).toBeLessThanOrEqual(30);
expect(row.description.length).toBeLessThanOrEqual(60); expect(row.description.length).toBeLessThanOrEqual(60);
expect(row.tooltip.length).toBeLessThanOrEqual(80);
} }
} }
}); });
test("has a titled row for every state the backend sends", () => { test("has a row for every state the backend sends", () => {
// A missing key here fails the typecheck, so a new state can't be left out. // A missing key here fails the typecheck, so a new state can't be left out.
const states: Record<HelperState, true> = { const states: Record<HelperState, true> = {
unavailable: true, unavailable: true,
@@ -106,7 +111,7 @@ describe("helperRow", () => {
}; };
for (const state of Object.keys(states) as HelperState[]) { for (const state of Object.keys(states) as HelperState[]) {
for (const backend of ["helper", "admin_prompt", "sudo"] as const) { for (const backend of ["helper", "admin_prompt", "sudo"] as const) {
expect(helperRow(state, backend).title).toMatch(/^Helper: \S/); expect(helperRow(state, backend).description).toMatch(/helper.*\.$/);
} }
} }
}); });
@@ -114,15 +119,15 @@ describe("helperRow", () => {
test("still gives a row for a state this build doesn't know", () => { test("still gives a row for a state this build doesn't know", () => {
const row = helperRow("from_the_future" as HelperState, "admin_prompt"); const row = helperRow("from_the_future" as HelperState, "admin_prompt");
expect(row).toEqual({ expect(row).toEqual({
title: "Helper: from_the_future", description: "The helper is from_the_future.",
description: "Changes use the password prompt.", tooltip: "The helper changes the system proxy without a password prompt.",
action: null, action: null,
}); });
}); });
test("says plainly that a dev build cannot use it", () => { test("says plainly that a dev build cannot use it", () => {
expect(helperRow("unavailable", "admin_prompt")).toMatchObject({ expect(helperRow("unavailable", "admin_prompt")).toMatchObject({
title: "Helper: not in dev builds", description: "The helper is not available in dev builds.",
action: null, action: null,
}); });
}); });
+26 -22
View File
@@ -28,7 +28,7 @@ export interface SystemProxySwitch {
/** What the system proxy switch shows */ /** What the system proxy switch shows */
export function systemProxySwitch(state: SystemProxyState): SystemProxySwitch { export function systemProxySwitch(state: SystemProxyState): SystemProxySwitch {
if (state.pending != null) { if (state.pending != null) {
const description = state.helper ? "Changing the system proxy" : "Waiting for your password"; const description = state.helper ? "Changing the system proxy" : "Waiting for macOS";
return { checked: state.pending, description, failed: false }; return { checked: state.pending, description, failed: false };
} }
// A failed change leaves the switch where the system proxy actually is. // A failed change leaves the switch where the system proxy actually is.
@@ -37,71 +37,75 @@ export function systemProxySwitch(state: SystemProxyState): SystemProxySwitch {
const cancelled = /cancel/i.test(state.error); const cancelled = /cancel/i.test(state.error);
return { return {
checked, checked,
description: cancelled ? "Cancelled in the password prompt" : state.error, description: cancelled ? "The password prompt was cancelled." : state.error,
failed: true, failed: true,
}; };
} }
return { return {
checked, checked,
description: checked description: checked
? "Routing this Mac's web traffic through Yaak Proxy" ? "Traffic from every app will be recorded."
: `Not routing. Only apps pointed at port ${state.port ?? 9090} are captured`, : `Only traffic sent to port ${state.port ?? 9090} will be recorded.`,
failed: false, failed: false,
}; };
} }
export interface HelperRow { export interface HelperRow {
title: string; /** The helper's state, in one sentence */
/** Which way changes go, and why */
description: string; description: string;
/** What the helper is for, or what this state still needs, behind the row's info icon */
tooltip: string;
/** What the row's button does, if it has one: the first two open the approval dialog */ /** What the row's button does, if it has one: the first two open the approval dialog */
action: "install" | "approve" | "unregister" | null; action: "install" | "approve" | "unregister" | null;
} }
/** The privileged helper's row in the state pill menu, kept short */ /** What the helper is for, the same in every state but one */
const HELPER_TOOLTIP = "The helper changes the system proxy without a password prompt.";
/** The privileged helper's row in Settings, kept short */
export function helperRow(helper: HelperState, backend: SystemProxyBackend): HelperRow { export function helperRow(helper: HelperState, backend: SystemProxyBackend): HelperRow {
switch (helper) { switch (helper) {
case "unavailable": case "unavailable":
return { return {
title: "Helper: not in dev builds", description: "The helper is not available in dev builds.",
description: "Changes use the password prompt.", tooltip: HELPER_TOOLTIP,
action: null, action: null,
}; };
case "not_found": case "not_found":
// What macOS reports until the helper is first installed. // What macOS reports until the helper is first installed.
return { return {
title: "Helper: not found by macOS", description: "macOS could not find the helper.",
description: "Changes use the password prompt.", tooltip: HELPER_TOOLTIP,
action: "install", action: "install",
}; };
case "not_registered": case "not_registered":
return { return {
title: "Helper: not installed", description: "The helper is not installed.",
description: "Changes use the password prompt.", tooltip: HELPER_TOOLTIP,
action: "install", action: "install",
}; };
case "requires_approval": case "requires_approval":
return { return {
title: "Helper: awaiting approval", description: "The helper is awaiting approval.",
description: "Changes use the password prompt.", tooltip: HELPER_TOOLTIP,
action: "approve", action: "approve",
}; };
case "enabled": case "enabled":
return { return {
title: "Helper: enabled", description: "The helper is installed.",
description: tooltip:
backend === "helper" backend === "helper"
? "No password prompts." ? HELPER_TOOLTIP
: "One more password prompt puts back your settings.", : "One more password prompt is needed to restore the system proxy settings.",
action: "unregister", action: "unregister",
}; };
default: { default: {
// Every state is handled above; one this build doesn't know still gets a row, rather // Every state is handled above; one this build doesn't know still gets a row, rather
// than taking the whole menu down. // than taking the whole page down.
const unknown: never = helper; const unknown: never = helper;
return { return {
title: `Helper: ${String(unknown)}`, description: `The helper is ${String(unknown)}.`,
description: "Changes use the password prompt.", tooltip: HELPER_TOOLTIP,
action: null, action: null,
}; };
} }
+1 -1
View File
@@ -672,7 +672,7 @@ droppedHeaders: Array<DroppedHeader>,
*/ */
keptHeaders: number, keptHeaders: number,
/** /**
* The body as it is saved: "JSON", "form URL-encoded with 7 fields". None without a body. * The body as it is saved: "JSON", "a URL-encoded form with 7 fields". None without a body.
*/ */
body: string | null, body: string | null,
/** /**
+1 -2
View File
@@ -12,8 +12,7 @@ use yaak_models::blob_manager::BlobManager;
const MESSAGE_PAGE: usize = 500; const MESSAGE_PAGE: usize = 500;
/// What a WebSocket's response body reads as: it has none. /// What a WebSocket's response body reads as: it has none.
pub const WEBSOCKET_BODY_NOTE: &str = pub const WEBSOCKET_BODY_NOTE: &str = "A WebSocket has no body";
"A WebSocket has no body after its 101 response. Its frames are its messages.";
/// Whether a flow's body is its stream's messages rather than bytes stored on their own: an /// Whether a flow's body is its stream's messages rather than bytes stored on their own: an
/// event stream's response, a gRPC call's request, and its response when the server answered /// event stream's response, a gRPC call's request, and its response when the server answered
+1 -1
View File
@@ -395,7 +395,7 @@ pub(crate) fn already_owned(owner: &ControlInfo) -> String {
pub(crate) fn owner_name(owner: Owner) -> &'static str { pub(crate) fn owner_name(owner: Owner) -> &'static str {
match owner { match owner {
Owner::App => "Yaak Proxy", Owner::App => "Yaak Proxy",
Owner::Cli => "the yaak CLI", Owner::Cli => "the CLI",
} }
} }
+6 -20
View File
@@ -65,14 +65,11 @@ pub fn flow_to_request(flow: &Flow, body: &DecodedBody) -> Conversion {
let (headers, dropped_headers) = split_headers(flow, body); let (headers, dropped_headers) = split_headers(flow, body);
let has_authorization = headers.iter().any(|h| h.name.eq_ignore_ascii_case("authorization")); let has_authorization = headers.iter().any(|h| h.name.eq_ignore_ascii_case("authorization"));
if has_authorization { if has_authorization {
warnings.push("The Authorization header is saved as captured, in plain text.".into()); warnings.push("The Authorization header is saved in plain text.".into());
} }
let (body_type, body_fields) = request_body(flow, body, &mut warnings); let (body_type, body_fields) = request_body(flow, body, &mut warnings);
if body.truncated { if body.truncated {
warnings.push(format!( warnings.push(format!("Only the first {} bytes of the body are saved.", body.data.len()));
"The body was cut at the capture limit, so only its first {} bytes are saved.",
body.data.len()
));
} }
// No name, as in Yaak, which then shows the URL. // No name, as in Yaak, which then shows the URL.
@@ -169,10 +166,7 @@ fn request_body(
let content_type = find_header(&flow.request_headers, "content-type").unwrap_or_default(); let content_type = find_header(&flow.request_headers, "content-type").unwrap_or_default();
let mime = content_type.split(';').next().unwrap_or_default().trim().to_ascii_lowercase(); let mime = content_type.split(';').next().unwrap_or_default().trim().to_ascii_lowercase();
let Ok(text) = std::str::from_utf8(&body.data) else { let Ok(text) = std::str::from_utf8(&body.data) else {
warnings.push( warnings.push("The body is binary and may be altered when saved.".into());
"The body is binary and is saved as text, so bytes that are not UTF-8 are replaced."
.into(),
);
return text_body(BODY_TYPE_OTHER, &String::from_utf8_lossy(&body.data)); return text_body(BODY_TYPE_OTHER, &String::from_utf8_lossy(&body.data));
}; };
@@ -195,9 +189,7 @@ fn request_body(
); );
} }
if mime == "multipart/form-data" { if mime == "multipart/form-data" {
warnings.push( warnings.push("The multipart body is saved as text, not form fields.".into());
"The multipart body is saved as text with its Content-Type, not as form fields.".into(),
);
} }
text_body(BODY_TYPE_OTHER, text) text_body(BODY_TYPE_OTHER, text)
} }
@@ -300,10 +292,7 @@ mod tests {
("Proxy-Authorization", DropReason::HopByHop), ("Proxy-Authorization", DropReason::HopByHop),
] ]
); );
assert_eq!( assert_eq!(c.warnings, vec!["The Authorization header is saved in plain text."]);
c.warnings,
vec!["The Authorization header is saved as captured, in plain text."]
);
} }
#[test] #[test]
@@ -463,9 +452,6 @@ mod tests {
let c = flow_to_request(&f, &data); let c = flow_to_request(&f, &data);
assert_eq!(names(&c.request), vec!["Content-Type"]); assert_eq!(names(&c.request), vec!["Content-Type"]);
assert_eq!(c.dropped_headers[0].reason, DropReason::BodyDecoded); assert_eq!(c.dropped_headers[0].reason, DropReason::BodyDecoded);
assert_eq!( assert_eq!(c.warnings, vec!["Only the first 4 bytes of the body are saved."]);
c.warnings,
vec!["The body was cut at the capture limit, so only its first 4 bytes are saved."]
);
} }
} }
+3 -7
View File
@@ -154,10 +154,7 @@ fn any_encrypted(data_dir: &Path) -> bool {
} }
fn missing_data_key(data_dir: &Path) -> String { fn missing_data_key(data_dir: &Path) -> String {
format!( format!("The encryption key is missing from {}.", data_dir.display())
"The proxy's captured traffic is encrypted, but its key is missing from {}",
data_dir.display()
)
} }
/// Both databases, opened together. /// Both databases, opened together.
@@ -324,12 +321,11 @@ pub fn encrypt_existing(
Ok(converted) Ok(converted)
} }
const KEY_UNAVAILABLE: &str = const KEY_UNAVAILABLE: &str = "Failed to read the encryption key from the keychain";
"Yaak Proxy stores traffic encrypted and couldn't read its key from the keychain";
/// Why the databases cannot be opened, or the CA made, without the data key. /// Why the databases cannot be opened, or the CA made, without the data key.
pub fn key_unavailable(reason: &str) -> String { pub fn key_unavailable(reason: &str) -> String {
format!("{KEY_UNAVAILABLE}: {reason}. Allow it access, then retry.") format!("{KEY_UNAVAILABLE}: {reason}")
} }
/// `reason` is [`key_unavailable`]'s: the keychain would not give up the key. /// `reason` is [`key_unavailable`]'s: the keychain would not give up the key.
@@ -18,7 +18,7 @@ pub struct FilterHistoryEntry {
} }
fn failed(what: &str, e: impl std::fmt::Display) -> String { fn failed(what: &str, e: impl std::fmt::Display) -> String {
format!("Failed to {what} the filter history: {e}") format!("Failed to {what} the search history: {e}")
} }
/// Every filter kept, most recently applied first. Empty for a database from before the /// Every filter kept, most recently applied first. Empty for a database from before the
+1 -1
View File
@@ -719,7 +719,7 @@ pub fn delete_all(db: &ProxyQueryManager) -> Result<(), String> {
} }
Ok::<_, yaak_database::Error>(()) Ok::<_, yaak_database::Error>(())
}) })
.map_err(|e| format!("Failed to delete captured traffic: {e}")) .map_err(|e| format!("Failed to delete recorded traffic: {e}"))
} }
/// Flows deleted per transaction while deleting cleared ones, so a flow being written never /// Flows deleted per transaction while deleting cleared ones, so a flow being written never
+10 -6
View File
@@ -387,7 +387,7 @@ async fn find_schema(
} }
Err(_) => Ok((None, Some(format!("No schema for {service} on {authority}")))), Err(_) => Ok((None, Some(format!("No schema for {service} on {authority}")))),
}, },
Err(e) => Ok((None, Some(format!("No schema for {service}; reflection failed: {e}")))), Err(e) => Ok((None, Some(format!("No schema for {service}. Reflection failed: {e}")))),
} }
} }
@@ -448,11 +448,15 @@ fn decode_with(
json: yaak_grpc::message_to_json(&decoded)?, json: yaak_grpc::message_to_json(&decoded)?,
message_type: Some(message_type), message_type: Some(message_type),
schema: Some(schema), schema: Some(schema),
note: message.truncated.then(|| "The message is cut at the capture limit".into()), note: message
.truncated
.then(|| "The message was truncated at the capture limit".into()),
}), }),
Err(e) => { Err(e) => Ok(schemaless(
Ok(schemaless(payload, message, Some(format!("Did not decode as {message_type}: {e}")))) payload,
} message,
Some(format!("Failed to decode as {message_type}: {e}")),
)),
} }
} }
@@ -460,7 +464,7 @@ fn schemaless(payload: &[u8], message: &FlowMessage, reason: Option<String>) ->
let mut notes: Vec<String> = reason.into_iter().collect(); let mut notes: Vec<String> = reason.into_iter().collect();
notes.push("Decoded without a schema: field names and types are guesses".into()); notes.push("Decoded without a schema: field names and types are guesses".into());
if message.truncated { if message.truncated {
notes.push("The message is cut at the capture limit".into()); notes.push("The message was truncated at the capture limit".into());
} }
let fields = decode_fields(payload, 0).unwrap_or_else(|| { let fields = decode_fields(payload, 0).unwrap_or_else(|| {
notes.push("Not a valid protobuf message: shown as bytes".into()); notes.push("Not a valid protobuf message: shown as bytes".into());
+9 -7
View File
@@ -153,12 +153,12 @@ mod imp {
} }
// SAFETY: unregisters the plist bundled with this app. // SAFETY: unregisters the plist bundled with this app.
unsafe { service.unregisterAndReturnError() } unsafe { service.unregisterAndReturnError() }
.map_err(|e| format!("Failed to remove the helper: {}", e.localizedDescription()))?; .map_err(|e| format!("Failed to uninstall the helper: {}", e.localizedDescription()))?;
Ok(state_of(&service)) Ok(state_of(&service))
} }
fn unavailable() -> String { fn unavailable() -> String {
"The helper is only available in the installed app, not a dev build".to_string() "The helper is not available in dev builds".to_string()
} }
/// Sends one request and reads its response. /// Sends one request and reads its response.
@@ -181,13 +181,15 @@ mod imp {
let _ = stream.set_write_timeout(Some(IO_TIMEOUT)); let _ = stream.set_write_timeout(Some(IO_TIMEOUT));
let mut line = serde_json::to_string(request).map_err(|e| e.to_string())?; let mut line = serde_json::to_string(request).map_err(|e| e.to_string())?;
line.push('\n'); line.push('\n');
(&stream).write_all(line.as_bytes()).map_err(|e| format!("The helper hung up: {e}"))?; (&stream)
.write_all(line.as_bytes())
.map_err(|e| format!("The helper disconnected: {e}"))?;
let mut reply = String::new(); let mut reply = String::new();
BufReader::new(&stream) BufReader::new(&stream)
.read_line(&mut reply) .read_line(&mut reply)
.map_err(|e| format!("The helper did not answer: {e}"))?; .map_err(|e| format!("The helper did not respond: {e}"))?;
let response: Response = serde_json::from_str(&reply) let response: Response = serde_json::from_str(&reply)
.map_err(|e| format!("The helper's answer was not understood: {e}"))?; .map_err(|e| format!("The helper sent an invalid response: {e}"))?;
response.into_result() response.into_result()
} }
@@ -214,12 +216,12 @@ pub fn state() -> HelperState {
#[cfg(not(target_os = "macos"))] #[cfg(not(target_os = "macos"))]
pub fn diagnose() -> String { pub fn diagnose() -> String {
"The helper is only available on macOS".to_string() "The helper is only supported on macOS".to_string()
} }
#[cfg(not(target_os = "macos"))] #[cfg(not(target_os = "macos"))]
pub fn register() -> Result<HelperState, String> { pub fn register() -> Result<HelperState, String> {
Err("The helper is only available on macOS".to_string()) Err("The helper is only supported on macOS".to_string())
} }
#[cfg(not(target_os = "macos"))] #[cfg(not(target_os = "macos"))]
+10 -13
View File
@@ -233,7 +233,7 @@ impl ProxyCtx {
fn lock_reason(&self) -> String { fn lock_reason(&self) -> String {
if self.encrypting().is_some() { if self.encrypting().is_some() {
return "traffic captured before this version is being encrypted.".to_string(); return "traffic recorded before this version is being encrypted.".to_string();
} }
let reason = self.lock_reason.lock().unwrap_or_else(PoisonError::into_inner).clone(); let reason = self.lock_reason.lock().unwrap_or_else(PoisonError::into_inner).clone();
reason.unwrap_or_else(|| "its key has not been read yet.".to_string()) reason.unwrap_or_else(|| "its key has not been read yet.".to_string())
@@ -275,8 +275,7 @@ impl ProxyCtx {
&& owner.pid != std::process::id() && owner.pid != std::process::id()
{ {
return Err(format!( return Err(format!(
"{}, so the traffic it stored can't be encrypted yet. Take over or quit it, then \ "{}. Take over or quit it, then retry.",
retry.",
control::already_owned(&owner) control::already_owned(&owner)
)); ));
} }
@@ -720,7 +719,7 @@ impl ProxyCtx {
.map_err(|e| e.to_string())?; .map_err(|e| e.to_string())?;
if !released { if !released {
return Err(format!( return Err(format!(
"The proxy in {} (pid {}) stopped but did not let go of it", "Failed to take over from {} (pid {})",
control::owner_name(owner.owner), control::owner_name(owner.owner),
owner.pid owner.pid
)); ));
@@ -808,7 +807,7 @@ impl ProxyCtx {
pub fn set_system_proxy(&self, enabled: bool) -> Result<SystemProxyStatus, String> { pub fn set_system_proxy(&self, enabled: bool) -> Result<SystemProxyStatus, String> {
let Some(port) = self.port() else { let Some(port) = self.port() else {
if enabled { if enabled {
return Err("The proxy is not running, so there is nothing to route to".into()); return Err("The proxy is not running".into());
} }
// Clears one left pointing at the configured port; asks for nothing otherwise. // Clears one left pointing at the configured port; asks for nothing otherwise.
self.change_system_proxy(false, self.settings()?.port, false)?; self.change_system_proxy(false, self.settings()?.port, false)?;
@@ -2246,7 +2245,7 @@ pub struct SavePreview {
/// Headers saved as captured, Authorization among them. /// Headers saved as captured, Authorization among them.
#[ts(type = "number")] #[ts(type = "number")]
pub kept_headers: usize, pub kept_headers: usize,
/// The body as it is saved: "JSON", "form URL-encoded with 7 fields". None without a body. /// The body as it is saved: "JSON", "a URL-encoded form with 7 fields". None without a body.
pub body: Option<String>, pub body: Option<String>,
/// What does not carry over exactly, one sentence each. /// What does not carry over exactly, one sentence each.
pub warnings: Vec<String>, pub warnings: Vec<String>,
@@ -2784,8 +2783,8 @@ async fn preview_save(ctx: ProxyCtx, req: PreviewSaveRequest) -> Result<SavePrev
"application/x-www-form-urlencoded" => { "application/x-www-form-urlencoded" => {
let fields = request.body.get("form").and_then(|f| f.as_array()).map_or(0, Vec::len); let fields = request.body.get("form").and_then(|f| f.as_array()).map_or(0, Vec::len);
match fields { match fields {
1 => "form URL-encoded with 1 field".to_string(), 1 => "a URL-encoded form with 1 field".to_string(),
n => format!("form URL-encoded with {n} fields"), n => format!("a URL-encoded form with {n} fields"),
} }
} }
_ => "text".to_string(), _ => "text".to_string(),
@@ -3184,8 +3183,7 @@ async fn count_saved_queries(
// -- Event loop -- // -- Event loop --
/// Why the engine cannot be taken over from another app. /// Why the engine cannot be taken over from another app.
pub const ANOTHER_APP_OWNS: &str = pub const ANOTHER_APP_OWNS: &str = "Quit the other Yaak Proxy to use this one.";
"Another Yaak Proxy window owns the engine. Quit it to use this one.";
/// How long taking over waits for the owner to let go of the engine. /// How long taking over waits for the owner to let go of the engine.
const TAKE_OVER_WAIT: Duration = Duration::from_secs(10); const TAKE_OVER_WAIT: Duration = Duration::from_secs(10);
@@ -3229,11 +3227,10 @@ const STOPPED_MID_STREAM: &str = "Yaak Proxy stopped before the stream ended";
const STOPPED_MID_TUNNEL: &str = "Yaak Proxy stopped before the connection closed"; const STOPPED_MID_TUNNEL: &str = "Yaak Proxy stopped before the connection closed";
/// Why a failed handshake's flow can't be saved as a request. /// Why a failed handshake's flow can't be saved as a request.
pub(crate) const NOT_A_REQUEST: &str = pub(crate) const NOT_A_REQUEST: &str = "The TLS handshake failed before any request was sent";
"Not a request: the client's TLS handshake failed before it sent one";
/// The error of a flow recording a client's refusal of the proxy's certificate. /// The error of a flow recording a client's refusal of the proxy's certificate.
const REJECTED_CERTIFICATE: &str = "Rejected the proxy's certificate"; const REJECTED_CERTIFICATE: &str = "Certificate rejected by the app";
/// A flow between its `RequestStart` and its end, with its bodies being written as they /// A flow between its `RequestStart` and its end, with its bodies being written as they
/// stream. /// stream.
+3 -3
View File
@@ -287,7 +287,7 @@ mod imp {
.map_err(|e| format!("Failed to run osascript: {e}"))?; .map_err(|e| format!("Failed to run osascript: {e}"))?;
// -128 is the dialog's Cancel button. // -128 is the dialog's Cancel button.
if String::from_utf8_lossy(&output.stderr).contains("(-128)") { if String::from_utf8_lossy(&output.stderr).contains("(-128)") {
return Err("Cancelled in the password dialog".to_string()); return Err("The password prompt was cancelled.".to_string());
} }
check(&output, "osascript") check(&output, "osascript")
} }
@@ -647,7 +647,7 @@ mod tests {
let dir = tempfile::tempdir().unwrap(); let dir = tempfile::tempdir().unwrap();
let path = dir.path().join(SNAPSHOT_FILE); let path = dir.path().join(SNAPSHOT_FILE);
let ns = fake(); let ns = fake();
let cancel = |_: &[Vec<String>]| Err("Cancelled in the password dialog".to_string()); let cancel = |_: &[Vec<String>]| Err("The password prompt was cancelled.".to_string());
assert!(set_scripted(&ns, cancel, true, 9090, &path).is_err()); assert!(set_scripted(&ns, cancel, true, 9090, &path).is_err());
assert!(!path.exists()); assert!(!path.exists());
assert!(ns.changes.lock().unwrap().is_empty()); assert!(ns.changes.lock().unwrap().is_empty());
@@ -663,7 +663,7 @@ mod tests {
set_scripted(&ns, runner(&ns, &prompts), true, 9090, &path).unwrap(); set_scripted(&ns, runner(&ns, &prompts), true, 9090, &path).unwrap();
// A second enable, the engine on a new port, must not save ours as the user's. // A second enable, the engine on a new port, must not save ours as the user's.
set_scripted(&ns, runner(&ns, &prompts), true, 9191, &path).unwrap(); set_scripted(&ns, runner(&ns, &prompts), true, 9191, &path).unwrap();
let cancel = |_: &[Vec<String>]| Err("Cancelled in the password dialog".to_string()); let cancel = |_: &[Vec<String>]| Err("The password prompt was cancelled.".to_string());
assert!(set_scripted(&ns, cancel, true, 9191, &path).is_err()); assert!(set_scripted(&ns, cancel, true, 9191, &path).is_err());
assert!(path.exists(), "a cancelled re-enable keeps the original snapshot"); assert!(path.exists(), "a cancelled re-enable keeps the original snapshot");
set_scripted(&ns, runner(&ns, &prompts), false, 9191, &path).unwrap(); set_scripted(&ns, runner(&ns, &prompts), false, 9191, &path).unwrap();
+7 -7
View File
@@ -88,14 +88,14 @@ pub fn schema() -> &'static Schema {
field("method", text(), "GET, POST, …", "@method:post"), field("method", text(), "GET, POST, …", "@method:post"),
field("status", number(), "Response status", "@status:4xx @status:>=400"), field("status", number(), "Response status", "@status:4xx @status:>=400"),
field("type", text(), "Content type", "@type:json"), field("type", text(), "Content type", "@type:json"),
field("app", text(), "App or program", "@app:Slack"), field("app", text(), "App", "@app:Slack"),
field( field(
"is", "is",
text(), text(),
"error, pending, system, encrypted, rejected, …", "error, pending, system, encrypted, rejected, …",
"@is:error -@is:system @is:encrypted", "@is:error -@is:system @is:encrypted",
), ),
map("header", "A header on either side", "@header.content-type:json"), map("header", "A request or response header", "@header.content-type:json"),
map("req.header", "A request header", "@req.header.accept:*"), map("req.header", "A request header", "@req.header.accept:*"),
map("res.header", "A response header", "@res.header.x-cache:hit"), map("res.header", "A response header", "@res.header.x-cache:hit"),
field("size", bytes(), "Response body size", "@size:>100kb"), field("size", bytes(), "Response body size", "@size:>100kb"),
@@ -109,8 +109,8 @@ pub fn schema() -> &'static Schema {
field("port", number(), "Port", "@port:8080"), field("port", number(), "Port", "@port:8080"),
field("proto", text(), "h1, h2, ws, sse, grpc", "@proto:h2"), field("proto", text(), "h1, h2, ws, sse, grpc", "@proto:h2"),
field("id", number(), "Flow number", "@id:>1200"), field("id", number(), "Flow number", "@id:>1200"),
field("cwd", text(), "Working folder", "@cwd:myapp"), field("cwd", text(), "Working directory", "@cwd:myapp"),
field("pid", number(), "Process id", "@pid:4121"), field("pid", number(), "Process ID", "@pid:4121"),
field( field(
"graphql.operation", "graphql.operation",
text(), text(),
@@ -119,9 +119,9 @@ pub fn schema() -> &'static Schema {
), ),
field("grpc.service", text(), "gRPC service", "@grpc.service:pkg.Greeter"), field("grpc.service", text(), "gRPC service", "@grpc.service:pkg.Greeter"),
field("grpc.method", text(), "gRPC method", "@grpc.method:SayHello"), field("grpc.method", text(), "gRPC method", "@grpc.method:SayHello"),
field("body", text(), "Not yet", ""), field("body", text(), "Not supported", ""),
field("req.body", text(), "Not yet", ""), field("req.body", text(), "Not supported", ""),
field("res.body", text(), "Not yet", ""), field("res.body", text(), "Not supported", ""),
], ],
FreeText::Substring, FreeText::Substring,
) )
@@ -391,7 +391,7 @@ fn encrypted_and_rejected() {
url: "https://my.1password.com".into(), url: "https://my.1password.com".into(),
client_http_version: "HTTP/1.1".into(), client_http_version: "HTTP/1.1".into(),
tls_rejected: Some("unknown_ca".into()), tls_rejected: Some("unknown_ca".into()),
error: Some("Rejected the proxy's certificate".into()), error: Some("Certificate rejected by the app".into()),
elapsed_ms: Some(3), elapsed_ms: Some(3),
started_at: (now() - Duration::minutes(5)).naive_utc(), started_at: (now() - Duration::minutes(5)).naive_utc(),
..Default::default() ..Default::default()
+1 -1
View File
@@ -44,7 +44,7 @@ pub fn open(vars: &[(String, String)]) -> Result<(), String> {
#[cfg(not(target_os = "macos"))] #[cfg(not(target_os = "macos"))]
pub fn open(_vars: &[(String, String)]) -> Result<(), String> { pub fn open(_vars: &[(String, String)]) -> Result<(), String> {
Err("Opening a terminal with the proxy set is only supported on macOS so far".to_string()) Err("Opening a terminal is only supported on macOS".to_string())
} }
#[cfg(test)] #[cfg(test)]
+6 -6
View File
@@ -189,7 +189,7 @@ fn stores_nothing_in_plaintext_and_reads_back_with_the_key() {
) )
.err() .err()
.unwrap(); .unwrap();
assert!(err.contains("couldn't read its key from the keychain"), "{err}"); assert!(err.contains("Failed to read the encryption key from the keychain"), "{err}");
// Nor without any key at all. // Nor without any key at all.
let raw = rusqlite::Connection::open(dir.path().join(PROXY_DB)).unwrap(); let raw = rusqlite::Connection::open(dir.path().join(PROXY_DB)).unwrap();
let err = raw.query_row("SELECT count(*) FROM flows", [], |r| r.get::<_, i64>(0)).unwrap_err(); let err = raw.query_row("SELECT count(*) FROM flows", [], |r| r.get::<_, i64>(0)).unwrap_err();
@@ -366,8 +366,8 @@ fn a_refused_key_refuses_capture() {
let err = ctx.start().unwrap_err(); let err = ctx.start().unwrap_err();
assert_eq!( assert_eq!(
err, err,
"Yaak Proxy stores traffic encrypted and couldn't read its key from the keychain: \ "Failed to read the encryption key from the keychain: Encryption error: User canceled \
Encryption error: User canceled the operation. Allow it access, then retry." the operation"
); );
assert_eq!(ctx.start_error().as_deref(), Some(err.as_str())); assert_eq!(ctx.start_error().as_deref(), Some(err.as_str()));
assert_eq!(ctx.port(), None); assert_eq!(ctx.port(), None);
@@ -426,8 +426,8 @@ fn waits_for_an_older_owner_before_converting() {
std::fs::write(&control, serde_json::to_string(&owner).unwrap()).unwrap(); std::fs::write(&control, serde_json::to_string(&owner).unwrap()).unwrap();
let ctx = open_ctx(dir.path(), MasterKeySource::memory()); let ctx = open_ctx(dir.path(), MasterKeySource::memory());
assert!(ctx.load_ca().err().unwrap().contains("can't be encrypted yet")); assert!(ctx.load_ca().err().unwrap().contains("Take over or quit it, then retry."));
assert!(ctx.start().unwrap_err().starts_with("The proxy is already running in the yaak CLI")); assert!(ctx.start().unwrap_err().starts_with("The proxy is already running in the CLI"));
for name in [PROXY_DB, BLOBS_DB] { for name in [PROXY_DB, BLOBS_DB] {
assert_eq!(file_state(&dir.path().join(name)).unwrap(), FileState::Plain); assert_eq!(file_state(&dir.path().join(name)).unwrap(), FileState::Plain);
} }
@@ -489,7 +489,7 @@ fn a_lost_data_key_is_not_replaced() {
std::fs::remove_file(dir.path().join(DATA_KEY)).unwrap(); std::fs::remove_file(dir.path().join(DATA_KEY)).unwrap();
let ctx = open_ctx(dir.path(), keys); let ctx = open_ctx(dir.path(), keys);
let err = ctx.start().unwrap_err(); let err = ctx.start().unwrap_err();
assert!(err.starts_with("The proxy's captured traffic is encrypted, but its key is missing")); assert!(err.starts_with("The encryption key is missing from"));
assert!(!dir.path().join(DATA_KEY).exists()); assert!(!dir.path().join(DATA_KEY).exists());
assert_eq!(std::fs::read_to_string(dir.path().join("ca/ca.pem")).unwrap(), pem); assert_eq!(std::fs::read_to_string(dir.path().join("ca/ca.pem")).unwrap(), pem);
assert!(ctx.db().is_err(), "locked"); assert!(ctx.db().is_err(), "locked");
+3 -6
View File
@@ -346,10 +346,7 @@ async fn save_writes_the_request_into_yaak() {
let saved = client.save(&flow.id, &target).await.unwrap(); let saved = client.save(&flow.id, &target).await.unwrap();
let dropped: Vec<&str> = saved.dropped_headers.iter().map(|h| h.name.as_str()).collect(); let dropped: Vec<&str> = saved.dropped_headers.iter().map(|h| h.name.as_str()).collect();
assert!(dropped.contains(&"host") && dropped.contains(&"content-length"), "{dropped:?}"); assert!(dropped.contains(&"host") && dropped.contains(&"content-length"), "{dropped:?}");
assert_eq!( assert_eq!(saved.warnings, vec!["The Authorization header is saved in plain text."]);
saved.warnings,
vec!["The Authorization header is saved as captured, in plain text."]
);
// The row is in Yaak's database, written as a sync so a running Yaak picks it up. // The row is in Yaak's database, written as a sync so a running Yaak picks it up.
let request = query_manager.connect().get_http_request(&saved.request.id).unwrap(); let request = query_manager.connect().get_http_request(&saved.request.id).unwrap();
@@ -443,7 +440,7 @@ async fn start_is_refused_while_another_process_owns_the_engine() {
std::fs::write(dir.path().join("control.json"), serde_json::to_vec(&owner).unwrap()).unwrap(); std::fs::write(dir.path().join("control.json"), serde_json::to_vec(&owner).unwrap()).unwrap();
let err = ctx.start().unwrap_err(); let err = ctx.start().unwrap_err();
assert!(err.contains("already running in the yaak CLI"), "{err}"); assert!(err.contains("already running in the CLI"), "{err}");
assert_eq!(ctx.port(), None); assert_eq!(ctx.port(), None);
daemon.kill().unwrap(); daemon.kill().unwrap();
@@ -471,7 +468,7 @@ async fn the_app_takes_over_from_the_cli() {
// The app opens onto the CLI's engine and cannot start its own. // The app opens onto the CLI's engine and cannot start its own.
let app = ctx(dir.path(), client_dir.path()); let app = ctx(dir.path(), client_dir.path());
assert!(app.serve_control_api().unwrap_err().contains("already running in the yaak CLI")); assert!(app.serve_control_api().unwrap_err().contains("already running in the CLI"));
let state = rpc(&app, "get_proxy_state", json!({})).await.unwrap(); let state = rpc(&app, "get_proxy_state", json!({})).await.unwrap();
assert_eq!(state["state"], "stopped"); assert_eq!(state["state"], "stopped");
assert_eq!(state["otherOwner"]["owner"], "cli"); assert_eq!(state["otherOwner"]["owner"], "cli");
@@ -164,12 +164,12 @@ async fn a_refused_handshake_is_one_flow_per_address() {
assert_eq!(rejected.url, url); assert_eq!(rejected.url, url);
assert_eq!(rejected.method, ""); assert_eq!(rejected.method, "");
assert_eq!(rejected.tls_rejected.as_deref(), Some("unknown_ca")); assert_eq!(rejected.tls_rejected.as_deref(), Some("unknown_ca"));
assert_eq!(rejected.error.as_deref(), Some("Rejected the proxy's certificate")); assert_eq!(rejected.error.as_deref(), Some("Certificate rejected by the app"));
assert!(!rejected.tunnelled); assert!(!rejected.tunnelled);
assert!(rejected.elapsed_ms.is_some(), "ended, not pending"); assert!(rejected.elapsed_ms.is_some(), "ended, not pending");
// No request was sent, so there is none to save. // No request was sent, so there is none to save.
let preview = rpc(&ctx, "preview_save", json!({"flowId": rejected.id})).await; let preview = rpc(&ctx, "preview_save", json!({"flowId": rejected.id})).await;
assert!(preview.unwrap_err().starts_with("Not a request")); assert!(preview.unwrap_err().starts_with("The TLS handshake failed"));
// Passed through from now on, naming the refusal. // Passed through from now on, naming the refusal.
let _tunnel = open_tunnel(&ctx, &target).await; let _tunnel = open_tunnel(&ctx, &target).await;
+1 -1
View File
@@ -448,7 +448,7 @@ async fn grpc_web_calls_are_stored_and_decoded() {
let decode = json!({ "flowId": flow.id, "seq": 1 }); let decode = json!({ "flowId": flow.id, "seq": 1 });
let schemaless = rpc(&ctx, "decode_grpc_message", decode.clone()).await.unwrap(); let schemaless = rpc(&ctx, "decode_grpc_message", decode.clone()).await.unwrap();
assert_eq!(schemaless["messageType"], Value::Null); assert_eq!(schemaless["messageType"], Value::Null);
assert!(schemaless["note"].as_str().unwrap().contains("reflection failed"), "{schemaless}"); assert!(schemaless["note"].as_str().unwrap().contains("Reflection failed"), "{schemaless}");
let fields: Value = serde_json::from_str(schemaless["json"].as_str().unwrap()).unwrap(); let fields: Value = serde_json::from_str(schemaless["json"].as_str().unwrap()).unwrap();
assert_eq!(fields[0]["string"], "Hello Ada"); assert_eq!(fields[0]["string"], "Hello Ada");
assert_eq!(fields[1], json!({ "field": 2, "wireType": "varint", "value": 3 })); assert_eq!(fields[1], json!({ "field": 2, "wireType": "varint", "value": 3 }));
+1 -1
View File
@@ -140,7 +140,7 @@ async fn cmd_open_settings(
let main = app_handle let main = app_handle
.get_webview_window(MAIN_WINDOW_LABEL) .get_webview_window(MAIN_WINDOW_LABEL)
.ok_or("The main window is not open to open Settings over")?; .ok_or("Failed to open Settings: the main window is closed")?;
let win = yaak_window::window::create_child_window( let win = yaak_window::window::create_child_window(
&main, &main,
"/", "/",
+2 -2
View File
@@ -184,7 +184,7 @@ impl GrpcParser {
message.truncated = true; message.truncated = true;
message.compressed = compressed; message.compressed = compressed;
message.error = Some(format!( message.error = Some(format!(
"The body ended partway through this message, {} of its {length} bytes in", "The body ended after {} of this message's {length} bytes.",
length - remaining length - remaining
)); ));
Some(message) Some(message)
@@ -318,7 +318,7 @@ mod tests {
assert_eq!(messages.len(), 1); assert_eq!(messages.len(), 1);
let cut = parser.end().expect("the unfinished message"); let cut = parser.end().expect("the unfinished message");
assert_eq!((&cut.data[..], cut.size, cut.truncated), (&b"hal"[..], 10, true)); assert_eq!((&cut.data[..], cut.size, cut.truncated), (&b"hal"[..], 10, true));
assert!(cut.error.as_deref().unwrap().contains("3 of its 10 bytes")); assert!(cut.error.as_deref().unwrap().contains("3 of this message's 10 bytes"));
// Nothing unfinished: nothing more. // Nothing unfinished: nothing more.
assert!(parser.end().is_none()); assert!(parser.end().is_none());
// A trailers frame cut short is still a trailers frame. // A trailers frame cut short is still a trailers frame.
+3 -3
View File
@@ -348,13 +348,13 @@ impl FrameParser {
Some(inflater) if !inflater.failed => { Some(inflater) if !inflater.failed => {
if let Err(e) = inflater.inflate(chunk, &mut message.payload) { if let Err(e) = inflater.inflate(chunk, &mut message.payload) {
inflater.failed = true; inflater.failed = true;
message.error = Some(format!("Failed to inflate the message: {e}")); message.error = Some(format!("Failed to decompress the message: {e}"));
} }
} }
Some(_) => { Some(_) => {
message.payload.push(chunk); message.payload.push(chunk);
message.error.get_or_insert_with(|| { message.error.get_or_insert_with(|| {
"Not inflated: an earlier message failed to, which lost the context".into() "This message was not decompressed because an earlier one failed.".into()
}); });
} }
None => message.payload.push(chunk), None => message.payload.push(chunk),
@@ -396,7 +396,7 @@ impl FrameParser {
&& let Err(e) = inflater.inflate(&DEFLATE_TAIL, &mut assembly.payload) && let Err(e) = inflater.inflate(&DEFLATE_TAIL, &mut assembly.payload)
{ {
inflater.failed = true; inflater.failed = true;
assembly.error = Some(format!("Failed to inflate the message: {e}")); assembly.error = Some(format!("Failed to decompress the message: {e}"));
} }
if inflater.no_context_takeover && !inflater.failed { if inflater.no_context_takeover && !inflater.failed {
inflater.decompress.reset(false); inflater.decompress.reset(false);
@@ -481,7 +481,7 @@ function StreamEventDetail({
/> />
{event.truncated || event.error ? ( {event.truncated || event.error ? (
<Banner color={event.error ? "danger" : "notice"} className="mb-2 text-sm"> <Banner color={event.error ? "danger" : "notice"} className="mb-2 text-sm">
{event.error ?? "The message is cut at the capture limit."} {event.error ?? "The message was truncated at the capture limit."}
</Banner> </Banner>
) : ( ) : (
<span aria-hidden /> <span aria-hidden />