paseo-readable-agent-activity 0.0.0-stage → 0.1.0-beta.11
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +91 -0
- package/LICENSE +21 -0
- package/OVERVIEW.md +7 -0
- package/README.md +178 -2
- package/UPSTREAM.md +31 -0
- package/client/activity.tsx +173 -0
- package/client/highlight.ts +108 -0
- package/client/transform.ts +26 -0
- package/images/01-json-comparison.png +0 -0
- package/images/02-code-comparison.png +0 -0
- package/images/03-preview-comparison.png +0 -0
- package/index.client.tsx +35 -0
- package/npm-shrinkwrap.json +75 -0
- package/package.json +43 -4
- package/paseo-plugin.json +6 -0
- package/shared/details.ts +144 -0
- package/shared/presentation.ts +141 -0
- package/shared/timeline.ts +42 -0
package/CHANGELOG.md
ADDED
|
@@ -0,0 +1,91 @@
|
|
|
1
|
+
# Changelog
|
|
2
|
+
|
|
3
|
+
## 0.1.0-beta.11 — 2026-10-18
|
|
4
|
+
|
|
5
|
+
- Publish on npm as `paseo-readable-agent-activity`, so the Paseo registry can track new versions without a manual pull request. The plugin ID stays `readable-agent-activity`; behavior is unchanged from beta.10.
|
|
6
|
+
- Move `prism-react-renderer` and `@getpaseo/protocol` to runtime dependencies. The client bundle compiles code from both, and an npm installation omits development dependencies. The package ships an `npm-shrinkwrap.json` for them, and its manifest has no build steps; the Git manifest keeps them.
|
|
7
|
+
- The packed package loaded on an isolated Paseo 0.12.0-beta.3 daemon, installed from npm through a local registry and from the unpacked directory. In the 0.12.0-beta.3 web client, DeepSeek Harness tool calls showed cards with the main argument on the collapsed header.
|
|
8
|
+
|
|
9
|
+
## 0.1.0-beta.10 — 2026-10-18
|
|
10
|
+
|
|
11
|
+
- Show the call's main argument after the tool name on the collapsed card header: the shell command, file path, search query or URL from the native tool detail, or the first of `command`, `cmd`, `file_path`, `filePath`, `path`, `pattern`, `query` and `url` in an unknown tool's input. It is one literal value, never a generated summary: the first line of a 200-character prefix, with `…` when anything was left out. It uses the muted monospace style and shrinks before the tool name.
|
|
12
|
+
- Typecheck, lint and 275 tests pass with SDK 0.9.2 and 0.11.4. Checked live on a Paseo 0.12.0-beta.2 daemon in the hosted web client with real DeepSeek Harness tool calls; the desktop app and native mobile were not checked.
|
|
13
|
+
|
|
14
|
+
## 0.1.0-beta.9 — 2026-09-29
|
|
15
|
+
|
|
16
|
+
- Add two Command Center actions: **Activity: use native tool rows (for Summary)** unregisters only the tool-call transformer, so Summary groups native tool rows again; **Activity: use tool cards (Full detail)** restores the cards. The choice is per client, not saved, and resets to cards when the plugin loads. The plugin still never reads or changes the host display preference.
|
|
17
|
+
- The Thinking adapter stays registered in both modes; reasoning rows never join Summary groups.
|
|
18
|
+
- Typecheck, lint and 253 tests pass with SDK 0.9.2 and 0.10.1, including Hermes bundle evaluation of the commands and a pinned 0.9.2 host test that regroups existing history after the switch. On a live 0.10.1 daemon in the hosted web client, the commands regrouped an existing conversation in Summary and restored the cards; the desktop app and mobile were not checked.
|
|
19
|
+
|
|
20
|
+
## 0.1.0-beta.8 — 2026-09-28
|
|
21
|
+
|
|
22
|
+
- Support Paseo 0.9.2 and 0.10.0-beta.1. The manifest now declares only the minimum `>=0.8.0`, following Paseo's requirements contract; the beta.7 `<0.10.0` bound made the 0.10.0-beta.1 daemon reject the plugin. Later releases are not tested.
|
|
23
|
+
- Pin the SDK development dependencies to 0.9.2. Typecheck, lint and all 252 tests pass with SDK 0.9.2 and with SDK 0.10.0-beta.1. Runtime behavior is unchanged; only code comments were updated.
|
|
24
|
+
- Refresh the 0.9 host fixtures from v0.9.2, byte-identical in v0.10.0-beta.1. Only assistant-message Markdown splitting changed upstream; tool-call projection and grouping did not. Summary remains unsupported. Beta.8 rendered live on a 0.10.0-beta.1 daemon in the hosted web client; the desktop app and native mobile checks are still pending.
|
|
25
|
+
|
|
26
|
+
## 0.1.0-beta.7 — 2026-09-18
|
|
27
|
+
|
|
28
|
+
- Support Paseo 0.9.0-beta.1 with updated SDK dependencies and a bounded compatibility range; retain 0.8 host regression fixtures.
|
|
29
|
+
- Keep native plan tools and structured plan cards out of the custom tool renderer, preserving 0.9 plan suppression and disclosure.
|
|
30
|
+
- Add unmodified 0.9 presentation fixtures for Full detail, Summary limitations, streaming/history identity, reasoning and speech. Summary remains unsupported: 0.9 retains all calls but does not group plugin cards.
|
|
31
|
+
|
|
32
|
+
## 0.1.0-beta.6 — 2026-09-13
|
|
33
|
+
|
|
34
|
+
- Unify ordinary and codex-conversion tools under Input / Output with one Show all / Show less. Preview twenty source lines and cap automatic wrapping to twenty displayed line-heights per section. No second character cap or independently scrolling output box.
|
|
35
|
+
- Fix long JSON strings/plain text/Thinking bypassing the preview limit: measure wrapped content and show the same Show all even when there are fewer than twenty source lines. Full text, highlighting, selection and manual streaming disclosure remain intact; preserve ancestor scrolling and scale the cap with system text size.
|
|
36
|
+
- Retain complete output metadata, traces and attachment data; only unwrap pure source/text transports. Preserve JSON numeric lexemes, duplicate keys, escapes and native detail fields.
|
|
37
|
+
- Highlight JSON keys/strings/literals with theme-aware, readable colors. Use one selectable text with actual newlines for copying.
|
|
38
|
+
- Remove the old Markdown reasoning renderer, generated summaries/diffs, separate tool rendering paths and Raw/Copy toolbars. Keep tool logos, host lifecycle status, non-JSON fallback and manual streaming disclosure. Remove the unused diff dependencies.
|
|
39
|
+
- Use a minimal Thinking disclosure row with original plain text. Match native default labels with 14px text, natural leading and 12px icons on both desktop and mobile; remove the trial's extra 44px mobile header minimum. Browser reference sequences measure 33px pitch versus 32px all-native (previous compact trial: 60px). Host gaps remain; header hit areas are smaller than native badges. Show all keeps its 44px compact target. No negative margins, body rewriting or restored Markdown parser.
|
|
40
|
+
- Add three side-by-side native-reference/plugin screenshots using the same synthetic data. Replace obsolete screenshot assets so Cafe does not display the old UI.
|
|
41
|
+
- Local trial accepted; 239 tests pass, including wrapped-preview regressions and real Hermes bundle evaluation. No new installation or daemon restart. Summary remains unsupported; see docs/verification.md for limits.
|
|
42
|
+
|
|
43
|
+
## 0.1.0-beta.5
|
|
44
|
+
|
|
45
|
+
- Move development and releases to `geoqiao/paseo-stuff`, subdirectory `plugins/agent-activity`, retaining the `readable-agent-activity` plugin ID. Old standalone tags remain unchanged; installed Git sources do not migrate automatically.
|
|
46
|
+
- Fix the native client loading path by replacing eager Shiki dependencies with the function-based, non-global Prism runtime from `prism-react-renderer`. Use its explicit ESM entry to work with Paseo's neutral bundler and eager CommonJS interop.
|
|
47
|
+
- Keep theme-derived token colors, exact text, the highlight/cache bounds and literal fallback. Shell highlighting uses a small local grammar; token classifications can differ from Shiki.
|
|
48
|
+
- Add complete-bundle evaluation tests, including the actual RN 0.81.5 Hermes executable, plus language/Unicode/CRLF/streaming/failure regressions. All 251 tests pass; native phone UI confirmation is still pending.
|
|
49
|
+
- Reloaded the existing enabled local installation successfully. This fix is included in the monorepo beta.5; the old standalone beta.4 tag does not include it.
|
|
50
|
+
|
|
51
|
+
## 0.1.0-beta.4
|
|
52
|
+
|
|
53
|
+
- Fix the missing Get Agent Activity header icon: replace the nonexistent `ListActivity` name with the supported `Activity` glyph. Formatting, spacing, summaries and disclosure behavior are unchanged.
|
|
54
|
+
- Validate all 62 configured Paseo tool icon names against Lucide 0.546.0 exports, including aliases, using a pinned test-only dependency. Production still uses the public host Icon with no added runtime dependency.
|
|
55
|
+
- Add four tool-name normalization cases and two renderer regressions for icon size/color and manual disclosure through status updates. All seven new cases failed before the fix; all 224 tests now pass.
|
|
56
|
+
- Browser checks cover real SVGs for all 62 mappings in wide dark/narrow light layouts, plus the existing UI suite. Local Paseo 0.8.0 reload succeeded; these browser checks are not native-mobile verification.
|
|
57
|
+
|
|
58
|
+
## 0.1.0-beta.3
|
|
59
|
+
|
|
60
|
+
- Share hunk-aware classification between diff colors and counts, so source lines beginning with `+++` or `---` are not confused with file headers. Accept whitespace-stripped blank context lines; preserve other partial/malformed input with a conservative fallback.
|
|
61
|
+
- Fix a Unicode boundary case that could duplicate an earlier readable preview segment; retain complete source and selected-view Copy.
|
|
62
|
+
- Expose complete edit details and read range metadata (including zero) in Raw, serialized only when selected rather than while building the default preview.
|
|
63
|
+
- Bound Exa/GitHub summary normalization before scanning large inputs, reusing the existing header helper. An all-whitespace prefix may omit the header summary; full input remains in details.
|
|
64
|
+
- Delete unreachable legacy output parsers and their obsolete tests; keep active recognition/summary behavior and add actual detail-source fidelity coverage.
|
|
65
|
+
- Avoid retaining a full classified diff array just to count changes; memoize selected Raw serialization so Copy/Show all do not repeatedly serialize large source details.
|
|
66
|
+
- 217 tests pass. The initial 213-case review suite reproduced 14 failures against the previous runtime; further regressions caught repeated Raw serialization and stripped blank context before fixes. Production-component browser checks cover dark/light, narrow layout, Raw/Copy, streaming, diff colors and preview boundaries; no native-mobile or latency benchmark claim.
|
|
67
|
+
|
|
68
|
+
## 0.1.0-beta.2
|
|
69
|
+
|
|
70
|
+
- Recognize only explicitly marked Pi Code-mode `exec_command` result wrappers and expand one output layer.
|
|
71
|
+
- Recognize the DSH-projected ACP `content[]` wrapper's explicit `type: "content"` / nested text blocks one layer, while keeping non-text blocks in Raw.
|
|
72
|
+
- Keep script, exit-code, running-session, empty-output, error and upstream-truncation status visible while preserving the complete wrapper in Raw.
|
|
73
|
+
- Render mixed content blocks with per-segment languages under one shared 20-line / 4,000-character preview budget; unknown blocks remain available in Raw.
|
|
74
|
+
- Add synthetic shared and renderer regressions for ACP mixed blocks/shared preview limits, malformed/large data, multiple results, lazy metadata/image tails, streaming disclosure and selected-view Copy.
|
|
75
|
+
- Preserve important script errors even when another status is present; report upstream truncation before malformed literal fallback without repairing it or repeating the warning.
|
|
76
|
+
- Keep segmented status spacing compact instead of mounting newline-only text boxes. Refresh synthetic screenshots and verify the production renderer in wide/narrow, dark/light browser layouts.
|
|
77
|
+
- 202 tests; read-only replay of 582 recent tool outputs checked shared preview bounds, no formatter exceptions and source immutability. No personal payloads were published.
|
|
78
|
+
|
|
79
|
+
## 0.1.0-beta.1
|
|
80
|
+
|
|
81
|
+
Initial public release of Readable Agent Activity, an MIT fork of Colorful Agent Activity.
|
|
82
|
+
|
|
83
|
+
- Quiet icons and bounded single-line summaries.
|
|
84
|
+
- Literal JavaScript inputs and lexical JSON formatting, including JSON inside text blocks.
|
|
85
|
+
- Readable typed tool-result content; one-layer UI label decoding and repeated-path compaction.
|
|
86
|
+
- 20-line / 4,000-character previews, manual Show all/Show less, full selected-view Copy and complete Raw.
|
|
87
|
+
- Defensive malformed/large data handling and stable manual disclosure through streaming.
|
|
88
|
+
- Explicit Full detail-only compatibility warning for Paseo 0.8.0.
|
|
89
|
+
- 187 regression tests, pinned Apache-2.0 host projection fixtures, synthetic screenshots and CI.
|
|
90
|
+
|
|
91
|
+
**Beta limitations:** Summary unsupported; all connected clients must use Full detail. Native mobile is untested. Full Show all/Raw/Copy can remain expensive.
|
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Matt Cowger
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
package/OVERVIEW.md
ADDED
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
Readable Agent Activity replaces Paseo's tool-call rows with expandable cards that show Input and Output as formatted, syntax-highlighted JSON, and gives Thinking rows a matching compact header. Long output is previewed at twenty lines, with Show all to expand it. Messages, approvals and the composer stay native.
|
|
2
|
+
|
|
3
|
+
Use the cards with Full detail. They bypass Summary grouping, so before switching to Summary, run "Activity: use native tool rows (for Summary)" from the Command Center on that client, and "Activity: use tool cards (Full detail)" to switch back. The choice applies to that client only and resets to cards whenever the plugin reloads. On Paseo 0.8, cards shown in Summary can hide the native group entry.
|
|
4
|
+
|
|
5
|
+
The plugin runs entirely on the client and reformats data Paseo already received, without summarizing or rebuilding diffs. It has no network, filesystem, process or daemon-side behavior. Show all on a very large payload can be slow.
|
|
6
|
+
|
|
7
|
+
Do not enable it next to another plugin that replaces tool-call or Thinking timeline items. It requires Paseo 0.8.0 or later. Its automated checks pass with the Paseo 0.11.2 SDK, and it was last exercised live on a 0.10.1 daemon in the hosted web client. The Paseo desktop app and native iOS and Android are untested. It is a fork of Matt Cowger's Colorful Agent Activity.
|
package/README.md
CHANGED
|
@@ -1,3 +1,179 @@
|
|
|
1
|
-
#
|
|
1
|
+
# Readable Agent Activity
|
|
2
2
|
|
|
3
|
-
|
|
3
|
+
**Formatted, highlighted tool-call JSON. Twenty-line previews. Recognizable tool icons.**
|
|
4
|
+
|
|
5
|
+
**Version 0.1.0-beta.11** · [Release notes](CHANGELOG.md) · Paseo 0.8 or later, checked through SDK 0.11.4. Tool cards need Full detail;
|
|
6
|
+
a Command Center action switches to native tool rows for Summary.
|
|
7
|
+
|
|
8
|
+
## What it does
|
|
9
|
+
|
|
10
|
+
- One **Input / Output** view for ordinary tools and codex-conversion.
|
|
11
|
+
- Format and syntax-highlight JSON. Non-JSON text stays readable; source-only
|
|
12
|
+
JavaScript inputs render in place.
|
|
13
|
+
- Preview at most **20 displayed lines per section**, including automatic wrapping. One **Show all / Show less**
|
|
14
|
+
control expands or collapses both sections. No Raw/Copy toolbar or extra explanations.
|
|
15
|
+
- Keep tool-specific icons and the host's running/failed/canceled status.
|
|
16
|
+
- Show the call's own main argument after the tool name on the collapsed header,
|
|
17
|
+
such as the shell command or file path: one literal value, first line only, in
|
|
18
|
+
muted monospace. It gives way before the tool name on narrow layouts.
|
|
19
|
+
- Keep expansion manual through streaming and theme updates. Text is selectable
|
|
20
|
+
with real newline characters for normal device copying.
|
|
21
|
+
|
|
22
|
+
Thinking uses a minimal matching foldable header to keep alternating tool/Thinking
|
|
23
|
+
rows compact. Its body is selectable original text, with twenty-line preview and
|
|
24
|
+
Show all, **without Markdown interpretation**. Messages, approvals and the composer
|
|
25
|
+
stay native. The plugin does not generate summaries, infer nested calls, rebuild
|
|
26
|
+
diffs or add settings screens.
|
|
27
|
+
|
|
28
|
+
## Before and after
|
|
29
|
+
|
|
30
|
+
Each pair uses **the same synthetic input and output**: original Paseo 0.8.0 reference
|
|
31
|
+
on the left, this plugin on the right. These are real renderer components in a browser
|
|
32
|
+
preview, not app/device captures. [Screenshot provenance and limits](docs/screenshots.md).
|
|
33
|
+
|
|
34
|
+
### Formatted, highlighted JSON
|
|
35
|
+
|
|
36
|
+
A serialized JSON result becomes an indented, colored structure.
|
|
37
|
+
|
|
38
|
+

|
|
39
|
+
|
|
40
|
+
### Source code without the JSON wrapper
|
|
41
|
+
|
|
42
|
+
A source-only exec input displays as code; a pure text result displays its JSON content.
|
|
43
|
+
Extra metadata and attachment fields are preserved when present.
|
|
44
|
+
|
|
45
|
+

|
|
46
|
+
|
|
47
|
+
### Twenty-line preview for long output
|
|
48
|
+
|
|
49
|
+
Non-JSON output stays plain text. Activity shows the first twenty lines and offers
|
|
50
|
+
one Show all for the complete result; wrapped long lines are bounded too.
|
|
51
|
+
|
|
52
|
+

|
|
53
|
+
|
|
54
|
+
## Data and limits
|
|
55
|
+
|
|
56
|
+
JSON strings are formatted by changing whitespace only: number spellings, duplicate
|
|
57
|
+
keys, key order and escapes remain intact. Structured objects are already decoded data.
|
|
58
|
+
|
|
59
|
+
Only a source-only code input and a pure single-text content envelope are unwrapped.
|
|
60
|
+
Extra input parameters, metadata, traces, unknown blocks and attachment data remain
|
|
61
|
+
visible in the formatted JSON. Arbitrary nested strings are not recursively decoded.
|
|
62
|
+
Native typed details receive a thin Input/Output adaptation; supplied edit fields
|
|
63
|
+
are retained, not replaced with a reconstructed diff.
|
|
64
|
+
|
|
65
|
+
**Show all displays the complete received representation**, not an inferred summary.
|
|
66
|
+
It cannot restore data already omitted upstream. The preview first takes twenty source
|
|
67
|
+
lines, then clips wrapping to twenty line-heights. It is not independently scrollable;
|
|
68
|
+
Show all removes both limits. There is no additional 4,000-character preview cap.
|
|
69
|
+
|
|
70
|
+
Formatting work is capped at 1,000,000 input/output characters, with bounded indentation;
|
|
71
|
+
larger or malformed text remains intact as literal text. Highlighting is capped at
|
|
72
|
+
100,000 characters. Source serialization, long-line layout and Show all can still be expensive for
|
|
73
|
+
large payloads, particularly a single very long line. This is not a zero-lag guarantee.
|
|
74
|
+
Virtual-list remounts reset local disclosure state.
|
|
75
|
+
|
|
76
|
+
[Conversion compatibility](docs/conversion-compatibility.md) · [Verification](docs/verification.md)
|
|
77
|
+
|
|
78
|
+
## Compatibility
|
|
79
|
+
|
|
80
|
+
**Paseo 0.8.0 or later · tool cards need Full detail · experimental beta.**
|
|
81
|
+
Beta.9 passes its checks with SDK 0.9.2, 0.10.1 and 0.11.2 and pinned 0.8.0/0.9.2 host
|
|
82
|
+
fixtures. No live 0.11 daemon was tested. Beta.8 rendered live on a 0.10.0-beta.1 daemon in the hosted web client with
|
|
83
|
+
Full detail. The manifest sets
|
|
84
|
+
only a minimum, `>=0.8.0`, as Paseo recommends; later releases are not tested.
|
|
85
|
+
|
|
86
|
+
Paseo 0.8 groups Summary calls before running plugin transformers. Its public API
|
|
87
|
+
exposes neither the display mode nor group members. An enabled replacement can
|
|
88
|
+
hide the native group's entry point; this plugin cannot automatically fall back.
|
|
89
|
+
|
|
90
|
+
Paseo 0.9 and 0.10.0-beta.1 transform each original call **before** Summary grouping.
|
|
91
|
+
All calls now remain visible, but plugin cards bypass grouping. Native plan/approval
|
|
92
|
+
tools pass through, preserving host suppression and expandable plan cards. No
|
|
93
|
+
display-mode API is available in any tested version, so the plugin cannot switch by itself.
|
|
94
|
+
|
|
95
|
+
### Switching between cards and native rows
|
|
96
|
+
|
|
97
|
+
Open the Command Center on the client you are using and choose:
|
|
98
|
+
|
|
99
|
+
- **Activity: use native tool rows (for Summary)** before selecting Summary. Tool calls
|
|
100
|
+
return to native rows, so Summary groups them again; Thinking keeps its compact row.
|
|
101
|
+
- **Activity: use tool cards (Full detail)** to restore the cards.
|
|
102
|
+
|
|
103
|
+
The choice applies only to that client and is not saved: tool cards return whenever
|
|
104
|
+
the plugin loads again, for example after an app reload. Switch each client separately.
|
|
105
|
+
The plugin does not detect or change the host display preference and does not inspect
|
|
106
|
+
private app state. With cards active, Summary shows every call ungrouped on 0.9+ and
|
|
107
|
+
can hide the group entry on 0.8. Do not enable two plugins replacing the same
|
|
108
|
+
tool-call timeline items.
|
|
109
|
+
|
|
110
|
+
Pinned 0.9.2 host-pipeline tests show that native rows regroup existing history and
|
|
111
|
+
that cards come back ungrouped. On a live 0.10.1 daemon in the hosted web client, the
|
|
112
|
+
commands regrouped an existing conversation in Summary and restored the cards.
|
|
113
|
+
0.8 re-rendering after a switch is untested; reload the conversation if rows do not
|
|
114
|
+
change. Hermes engine tests and compact browser previews are not on-device mobile
|
|
115
|
+
UI tests. [Compatibility evidence](docs/compatibility.md).
|
|
116
|
+
|
|
117
|
+
## Install and local development
|
|
118
|
+
|
|
119
|
+
Plugins are **trusted, unsandboxed code**. This plugin is client-only and uses public
|
|
120
|
+
SDK modules, React Native primitives and host theme colors. It has no runtime network,
|
|
121
|
+
filesystem, process or daemon-side behavior. Contributions unregister on cleanup.
|
|
122
|
+
|
|
123
|
+
On Paseo 0.11 or later, install the reviewed [registry](https://paseo.sh/plugins) entry on
|
|
124
|
+
the intended daemon. Its registry ID is `geoqiao/agent-activity`; the installed plugin ID
|
|
125
|
+
stays `readable-agent-activity`:
|
|
126
|
+
|
|
127
|
+
```sh
|
|
128
|
+
paseo plugin add geoqiao/agent-activity --host <your-host>
|
|
129
|
+
```
|
|
130
|
+
|
|
131
|
+
To install the published npm package directly:
|
|
132
|
+
|
|
133
|
+
```sh
|
|
134
|
+
paseo plugin add npm:paseo-readable-agent-activity@0.1.0-beta.11 --host <your-host>
|
|
135
|
+
```
|
|
136
|
+
|
|
137
|
+
Paseo installs the package's two runtime dependencies, `prism-react-renderer` and
|
|
138
|
+
`@getpaseo/protocol`, from its `npm-shrinkwrap.json`; both are compiled into the
|
|
139
|
+
client bundle. The package's manifest has no build steps. The `paseo-plugin.json` in
|
|
140
|
+
the Git source keeps them: they install the development dependencies and run the checks.
|
|
141
|
+
|
|
142
|
+
To install the pinned prerelease directly from Git instead:
|
|
143
|
+
|
|
144
|
+
```sh
|
|
145
|
+
paseo plugin add git:geoqiao/paseo-stuff:plugins/agent-activity --ref readable-agent-activity-v0.1.0-beta.11 --host <your-host>
|
|
146
|
+
```
|
|
147
|
+
|
|
148
|
+
Paseo 0.11 reads a bare `owner/name` source as a registry ID, so the `git:` prefix is
|
|
149
|
+
required there. Paseo CLIs before 0.11 take the same Git source without the prefix.
|
|
150
|
+
|
|
151
|
+
Installation enables the plugin with tool cards active. Review its source/trust
|
|
152
|
+
requirements, then use Full detail or switch to native tool rows first. Do not create a duplicate installation. Existing installations
|
|
153
|
+
may use a runtime alias such as `colorful-agent-activity`; manage that existing ID.
|
|
154
|
+
[Repository migration notes](https://github.com/geoqiao/paseo-stuff/blob/main/MIGRATION.md).
|
|
155
|
+
|
|
156
|
+
For local development, run from the plugin directory (Node.js 22+):
|
|
157
|
+
|
|
158
|
+
```sh
|
|
159
|
+
npm ci --ignore-scripts --legacy-peer-deps --no-audit --no-fund
|
|
160
|
+
npm run check
|
|
161
|
+
# Only if the existing installation is enabled:
|
|
162
|
+
paseo plugin reload <existing-runtime-id> --host <your-host>
|
|
163
|
+
paseo plugin ls --host <your-host>
|
|
164
|
+
```
|
|
165
|
+
|
|
166
|
+
Preserve disabled installations; do not auto-enable them. No daemon restart is needed.
|
|
167
|
+
Tests cover formatter fidelity, malformed/large data, complete output, text selection,
|
|
168
|
+
manual disclosure, streaming, icons, theme colors, the Hermes bundle and pinned host
|
|
169
|
+
projection. Native iOS/Android UI and the complete reconnect matrix remain unverified.
|
|
170
|
+
|
|
171
|
+
## Credits
|
|
172
|
+
|
|
173
|
+
An MIT fork of Matt Cowger's [Colorful Agent Activity](https://github.com/mcowger/paseo-plugins/tree/91058be73840ae11b130b6bb7d34b07652462217/colorful-agent-activity).
|
|
174
|
+
Original copyright retained: [LICENSE](LICENSE), [provenance](UPSTREAM.md).
|
|
175
|
+
Vendored Paseo test fixtures retain their separate Apache-2.0 license.
|
|
176
|
+
|
|
177
|
+
The three comparison images show beta.6. Earlier screenshots remain in historical release tags.
|
|
178
|
+
Published on npm as `paseo-readable-agent-activity` from 0.1.0-beta.11.
|
|
179
|
+
[paseo.cafe submission and migration status](docs/catalog.md).
|
package/UPSTREAM.md
ADDED
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
# Upstream provenance
|
|
2
|
+
|
|
3
|
+
## Plugin
|
|
4
|
+
|
|
5
|
+
- Repository: https://github.com/mcowger/paseo-plugins
|
|
6
|
+
- Commit: `91058be73840ae11b130b6bb7d34b07652462217`
|
|
7
|
+
- Subdirectory: `colorful-agent-activity`
|
|
8
|
+
- License: MIT, Copyright (c) 2026 Matt Cowger; original LICENSE retained.
|
|
9
|
+
- Imported from an installed local checkout, not a fresh upstream snapshot.
|
|
10
|
+
- Local changes already present at import: tools/reasoning default collapsed, manual-only expansion during streaming, React renderer regression tests (44 total tests), documentation and development-only test dependencies.
|
|
11
|
+
|
|
12
|
+
This fork publishes under the distinct manifest ID **readable-agent-activity**. Existing local installations may retain an explicit runtime alias; internal renderer kind strings remain unchanged. Do not enable this fork and another timeline-replacement plugin together.
|
|
13
|
+
|
|
14
|
+
## Changes in this fork
|
|
15
|
+
|
|
16
|
+
- Beta.6: one Input/Output formatter, twenty-line previews, a shared Show all, source-preserving JSON highlighting and tool icons.
|
|
17
|
+
- Manual-only expansion; minimal plain-text Thinking disclosure matches tool-row density. Messages remain native. The old Markdown reasoning parser is not restored.
|
|
18
|
+
- Thin source/text transport adaptation retains metadata, traces, attachments and native detail fields. Malformed and oversized text falls back without silent truncation.
|
|
19
|
+
- Earlier betas included summaries, reconstructed diffs, UI-label compaction and Raw/Copy controls. Those helpers and their obsolete tests are removed in beta.6, not retained behind hidden UI.
|
|
20
|
+
- No server entry; presentation uses public SDK modules and does not need daemon-side privileges.
|
|
21
|
+
- Additional pure/renderer tests and pinned host compatibility fixtures. Full detail only; Summary remains unsupported.
|
|
22
|
+
|
|
23
|
+
Public screenshots are captured from synthetic data, not personal conversations. Their
|
|
24
|
+
left columns use a source-adapted Paseo native reference, not the upstream Activity
|
|
25
|
+
plugin. See [screenshot provenance](docs/screenshots.md) for exact source and limitations.
|
|
26
|
+
|
|
27
|
+
## Host test fixtures
|
|
28
|
+
|
|
29
|
+
Four unmodified Paseo v0.8.0 source files from commit `b8e24677e12b226c7c38c1c3a40649daa9f1152f`, under Apache-2.0, are used only in tests. Their license is not replaced by the plugin's MIT license.
|
|
30
|
+
|
|
31
|
+
See [fixture provenance](tests/fixtures/paseo-0.8/README.md) and [fixture LICENSE](tests/fixtures/paseo-0.8/LICENSE).
|
|
@@ -0,0 +1,173 @@
|
|
|
1
|
+
import type { PluginTimelineItemProps } from "@getpaseo/plugin/client";
|
|
2
|
+
import { Icon, ScrollView } from "@getpaseo/plugin/client/react-native";
|
|
3
|
+
import { Fragment, useMemo, useState, type ReactNode } from "react";
|
|
4
|
+
import { ActivityIndicator, Platform, Pressable, Text, View, useWindowDimensions, type TextStyle, type ViewStyle } from "react-native";
|
|
5
|
+
import { detailSections, presentValue, previewText, PREVIEW_LINES } from "../shared/details";
|
|
6
|
+
import { toolArgument } from "../shared/presentation";
|
|
7
|
+
import type { ReasoningItemData, ToolCallItemData } from "../shared/timeline";
|
|
8
|
+
import { useSyntaxTokens } from "./highlight";
|
|
9
|
+
|
|
10
|
+
type Theme = PluginTimelineItemProps["theme"];
|
|
11
|
+
type Layout = PluginTimelineItemProps["layout"];
|
|
12
|
+
|
|
13
|
+
function useStyles(theme: Theme, layout: Layout) {
|
|
14
|
+
return useMemo(() => {
|
|
15
|
+
const { colors } = theme;
|
|
16
|
+
return {
|
|
17
|
+
row: { minWidth: 0 } satisfies ViewStyle,
|
|
18
|
+
// Native 0.8 labels use 14px with natural leading on every platform.
|
|
19
|
+
// Host plugin rows already add 16px between cards: don't add a mobile
|
|
20
|
+
// 44px minimum on top. Let larger system text grow the header naturally.
|
|
21
|
+
header: { flexDirection: "row", alignItems: "center", gap: 8, minWidth: 0,
|
|
22
|
+
minHeight: 16, paddingHorizontal: 2,
|
|
23
|
+
paddingVertical: 0, borderRadius: 4 } satisfies ViewStyle,
|
|
24
|
+
title: { color: colors.foreground, fontSize: 14,
|
|
25
|
+
flexShrink: 1, minWidth: 0 } satisfies TextStyle,
|
|
26
|
+
// The argument gives way first, so a long command never hides the tool name.
|
|
27
|
+
argument: { color: colors.foregroundMuted, fontFamily: "monospace", fontSize: 12,
|
|
28
|
+
flex: 1, flexShrink: 100, minWidth: 0 } satisfies TextStyle,
|
|
29
|
+
body: { marginLeft: 20, paddingTop: 4, paddingBottom: 8, gap: 12, minWidth: 0 } satisfies ViewStyle,
|
|
30
|
+
section: { gap: 5, minWidth: 0 } satisfies ViewStyle,
|
|
31
|
+
label: { color: colors.foregroundMuted, fontSize: 11, lineHeight: 18 } satisfies TextStyle,
|
|
32
|
+
status: { color: colors.foregroundMuted, fontSize: 11 } satisfies TextStyle,
|
|
33
|
+
action: { alignSelf: "flex-start", paddingHorizontal: 5,
|
|
34
|
+
minHeight: layout.compact ? 44 : 24, paddingVertical: layout.compact ? 12 : 3, borderRadius: 3 } satisfies ViewStyle,
|
|
35
|
+
code: { backgroundColor: colors.surface1, padding: 10, borderRadius: 4, minWidth: "100%" } satisfies ViewStyle,
|
|
36
|
+
codeText: { color: colors.foreground, fontFamily: "monospace", fontSize: 12, lineHeight: 19 } satisfies TextStyle,
|
|
37
|
+
};
|
|
38
|
+
}, [theme, layout.compact]);
|
|
39
|
+
}
|
|
40
|
+
type Styles = ReturnType<typeof useStyles>;
|
|
41
|
+
|
|
42
|
+
function Button({ label, children, onPress, theme, style, expanded }: {
|
|
43
|
+
label: string; children: ReactNode; onPress: () => void; theme: Theme; style: ViewStyle; expanded: boolean;
|
|
44
|
+
}) {
|
|
45
|
+
const [hovered, setHovered] = useState(false);
|
|
46
|
+
const [focused, setFocused] = useState(false);
|
|
47
|
+
return <Pressable accessibilityRole="button" accessibilityLabel={label}
|
|
48
|
+
accessibilityState={{ expanded }} onPress={onPress}
|
|
49
|
+
onHoverIn={() => setHovered(true)} onHoverOut={() => setHovered(false)}
|
|
50
|
+
onFocus={() => setFocused(true)} onBlur={() => setFocused(false)}
|
|
51
|
+
style={({ pressed }) => [style, (hovered || pressed) && { backgroundColor: theme.colors.surface1 },
|
|
52
|
+
focused && { outlineColor: theme.colors.accent, outlineWidth: 1, outlineStyle: "solid" }]}>
|
|
53
|
+
{children}
|
|
54
|
+
</Pressable>;
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
/** Measure natural content, but don't let the preview become a nested scroll owner. */
|
|
58
|
+
function Preview({ all, lineHeight, onOverflow, children }: {
|
|
59
|
+
all: boolean; lineHeight: number; onOverflow: (overflow: boolean) => void; children: ReactNode;
|
|
60
|
+
}) {
|
|
61
|
+
const { fontScale } = useWindowDimensions();
|
|
62
|
+
const maxHeight = PREVIEW_LINES * lineHeight * fontScale;
|
|
63
|
+
// RN Web's scrollEnabled=false also cancels ancestor wheel/touch gestures.
|
|
64
|
+
// Hidden overflow prevents web scrolling without intercepting the host scroll.
|
|
65
|
+
return all ? children : <ScrollView scrollEnabled={Platform.OS === "web"}
|
|
66
|
+
showsVerticalScrollIndicator={false} showsHorizontalScrollIndicator={false}
|
|
67
|
+
style={{ maxHeight, flexGrow: 0, overflow: "hidden" }}
|
|
68
|
+
onContentSizeChange={(_width, height) => onOverflow(height > maxHeight + 0.5)}>
|
|
69
|
+
{children}
|
|
70
|
+
</ScrollView>;
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
function CodeContent({ text, language, theme, styles, all, onOverflow }: {
|
|
74
|
+
text: string; language: string; theme: Theme; styles: Styles;
|
|
75
|
+
all: boolean; onOverflow: (overflow: boolean) => void;
|
|
76
|
+
}) {
|
|
77
|
+
const tokens = useSyntaxTokens(text, language, theme.colors);
|
|
78
|
+
const wrap = language === "text" || language === "json";
|
|
79
|
+
const content = <View style={[styles.code, wrap && { minWidth: 0, width: "100%" }]}>
|
|
80
|
+
{/* One selectable text, including real newline characters, keeps copying faithful. */}
|
|
81
|
+
<Preview all={all} lineHeight={styles.codeText.lineHeight} onOverflow={onOverflow}>
|
|
82
|
+
<Text selectable style={styles.codeText}>
|
|
83
|
+
{tokens ? tokens.map((line, index) => <Fragment key={index}>
|
|
84
|
+
{index > 0 ? "\n" : null}
|
|
85
|
+
{line.map((token, tokenIndex) => <Text key={tokenIndex}
|
|
86
|
+
style={{ color: token.color ?? theme.colors.foreground }}>{token.content}</Text>)}
|
|
87
|
+
</Fragment>) : text}
|
|
88
|
+
</Text>
|
|
89
|
+
</Preview>
|
|
90
|
+
</View>;
|
|
91
|
+
return wrap ? content : <ScrollView horizontal nestedScrollEnabled
|
|
92
|
+
contentContainerStyle={{ flexGrow: 1 }}>{content}</ScrollView>;
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
function Details({ data, all, toggleAll, theme, styles }: {
|
|
96
|
+
data: ToolCallItemData; all: boolean; toggleAll: () => void; theme: Theme; styles: Styles;
|
|
97
|
+
}) {
|
|
98
|
+
const [inputOverflow, setInputOverflow] = useState(false);
|
|
99
|
+
const [outputOverflow, setOutputOverflow] = useState(false);
|
|
100
|
+
const sections = useMemo(() => detailSections(data).map(section => {
|
|
101
|
+
const full = presentValue(section.value, section.language);
|
|
102
|
+
return { ...section, full, preview: previewText(full.text) };
|
|
103
|
+
}), [data]);
|
|
104
|
+
return <View style={styles.body}>
|
|
105
|
+
{sections.map(section => <View key={section.label} style={styles.section}>
|
|
106
|
+
<Text style={styles.label}>{section.label}</Text>
|
|
107
|
+
{section.full.text === "" ? <Text style={styles.label}>
|
|
108
|
+
{data.status === "running" && section.label === "Output" && section.value === undefined
|
|
109
|
+
? "Waiting for output…" : "Empty"}
|
|
110
|
+
</Text> : <CodeContent text={all ? section.full.text : section.preview.text}
|
|
111
|
+
language={section.full.language} theme={theme} styles={styles} all={all}
|
|
112
|
+
onOverflow={section.label === "Input" ? setInputOverflow : setOutputOverflow} />}
|
|
113
|
+
</View>)}
|
|
114
|
+
{all || sections.some(section => section.preview.truncated || (section.full.text !== ""
|
|
115
|
+
&& (section.label === "Input" ? inputOverflow : outputOverflow))) ? <Button
|
|
116
|
+
label={all ? "Show less" : "Show all"} expanded={all} onPress={toggleAll}
|
|
117
|
+
theme={theme} style={styles.action}><Text style={styles.label}>{all ? "Show less" : "Show all"}</Text></Button> : null}
|
|
118
|
+
</View>;
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
export function ToolActivity({ item, theme, layout }: PluginTimelineItemProps<ToolCallItemData>) {
|
|
122
|
+
const styles = useStyles(theme, layout);
|
|
123
|
+
const [expanded, setExpanded] = useState(false);
|
|
124
|
+
const [all, setAll] = useState(false);
|
|
125
|
+
const data = item.data;
|
|
126
|
+
const argument = useMemo(() => toolArgument(data.detail), [data.detail]);
|
|
127
|
+
return <View style={styles.row}>
|
|
128
|
+
<Button label={(expanded ? "Collapse " : "Expand ") + data.name} expanded={expanded}
|
|
129
|
+
onPress={() => setExpanded(value => !value)} theme={theme} style={styles.header}>
|
|
130
|
+
<Icon name={data.presentation.icon} size={12} color={theme.colors.foregroundMuted} />
|
|
131
|
+
<Text numberOfLines={1} style={styles.title}>{data.name}</Text>
|
|
132
|
+
{argument ? <Text numberOfLines={1} style={styles.argument}>{argument}</Text> : <View style={{ flex: 1 }} />}
|
|
133
|
+
{data.status === "running" ? <ActivityIndicator accessibilityLabel="Running"
|
|
134
|
+
color={theme.colors.foregroundMuted} size="small"
|
|
135
|
+
style={{ width: 14, height: 14, transform: [{ scale: 0.65 }] }} />
|
|
136
|
+
: data.status === "failed" ? <Text style={[styles.status, { color: theme.colors.statusDanger }]}>Failed</Text>
|
|
137
|
+
: data.status === "canceled" ? <Text style={styles.status}>Canceled</Text> : null}
|
|
138
|
+
</Button>
|
|
139
|
+
{expanded ? <Details data={data} all={all} toggleAll={() => setAll(value => !value)}
|
|
140
|
+
theme={theme} styles={styles} /> : null}
|
|
141
|
+
</View>;
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
/** Minimal density adapter, not the former Markdown/reasoning presentation. */
|
|
145
|
+
export function ReasoningActivity({ item, theme, layout }: PluginTimelineItemProps<ReasoningItemData>) {
|
|
146
|
+
const styles = useStyles(theme, layout);
|
|
147
|
+
const [expanded, setExpanded] = useState(false);
|
|
148
|
+
const [all, setAll] = useState(false);
|
|
149
|
+
const [overflow, setOverflow] = useState(false);
|
|
150
|
+
const preview = expanded && !all ? previewText(item.data.text) : { text: "", truncated: false };
|
|
151
|
+
return <View style={styles.row}>
|
|
152
|
+
<Button label={expanded ? "Collapse Thinking" : "Expand Thinking"} expanded={expanded}
|
|
153
|
+
onPress={() => setExpanded(value => !value)} theme={theme} style={styles.header}>
|
|
154
|
+
<Icon name="Brain" size={12} color={theme.colors.foregroundMuted} />
|
|
155
|
+
<Text numberOfLines={1} style={[styles.title, { color: theme.colors.foregroundMuted }]}>Thinking</Text>
|
|
156
|
+
<View style={{ flex: 1 }} />
|
|
157
|
+
{item.data.phase === "streaming" ? <ActivityIndicator accessibilityLabel="Running"
|
|
158
|
+
color={theme.colors.foregroundMuted} size="small"
|
|
159
|
+
style={{ width: 14, height: 14, transform: [{ scale: 0.65 }] }} /> : null}
|
|
160
|
+
</Button>
|
|
161
|
+
{expanded ? <View style={styles.body}>
|
|
162
|
+
<Preview all={all} lineHeight={20} onOverflow={setOverflow}>
|
|
163
|
+
<Text selectable style={{ color: theme.colors.foreground, fontSize: 13, lineHeight: 20 }}>
|
|
164
|
+
{all ? item.data.text : preview.text}
|
|
165
|
+
</Text>
|
|
166
|
+
</Preview>
|
|
167
|
+
{all || preview.truncated || (item.data.text !== "" && overflow) ? <Button label={all ? "Show less" : "Show all"} expanded={all}
|
|
168
|
+
onPress={() => setAll(value => !value)} theme={theme} style={styles.action}>
|
|
169
|
+
<Text style={styles.label}>{all ? "Show less" : "Show all"}</Text>
|
|
170
|
+
</Button> : null}
|
|
171
|
+
</View> : null}
|
|
172
|
+
</View>;
|
|
173
|
+
}
|
|
@@ -0,0 +1,108 @@
|
|
|
1
|
+
// Use the ESM distribution: Paseo's neutral bundler ignores main/module fields,
|
|
2
|
+
// and its eager CJS interop would snapshot Prism before CJS initialization.
|
|
3
|
+
import { Prism } from "prism-react-renderer/dist/index.mjs";
|
|
4
|
+
import { useEffect, useState } from "react";
|
|
5
|
+
|
|
6
|
+
export interface SyntaxColors {
|
|
7
|
+
foreground: string;
|
|
8
|
+
foregroundMuted: string;
|
|
9
|
+
accent: string;
|
|
10
|
+
surface1: string;
|
|
11
|
+
statusSuccess?: string;
|
|
12
|
+
statusWarning?: string;
|
|
13
|
+
}
|
|
14
|
+
export interface SyntaxToken {
|
|
15
|
+
content: string;
|
|
16
|
+
color?: string;
|
|
17
|
+
}
|
|
18
|
+
export const MAX_HIGHLIGHT_CHARS = 100_000;
|
|
19
|
+
// This vendored Prism instance has no global registration or automatic DOM scan.
|
|
20
|
+
// Use tokenize only, never its HTML/React renderer. Its function-based runtime
|
|
21
|
+
// also works in Hermes when Paseo evaluates the bundle without Metro transforms.
|
|
22
|
+
// Bash is not bundled by prism-react-renderer; this deliberately small grammar
|
|
23
|
+
// colors shell strings/comments/control words without claiming full shell parsing.
|
|
24
|
+
Prism.languages.bash = {
|
|
25
|
+
comment: { pattern: /(^|[\t ])#.*/m, lookbehind: true, greedy: true },
|
|
26
|
+
string: { pattern: /"(?:\\[\s\S]|[^"\\])*"|'[^']*'/, greedy: true },
|
|
27
|
+
variable: /\$(?:\w+|\{[^}\r\n]*\}|[?$!#@*-])/,
|
|
28
|
+
keyword: /\b(?:if|then|else|elif|fi|for|while|until|do|done|case|esac|in|function|select|return|export|local)\b/,
|
|
29
|
+
operator: /&&|\|\||[|&;<>]/,
|
|
30
|
+
punctuation: /[(){}[\]]/,
|
|
31
|
+
};
|
|
32
|
+
const supported = new Set(["bash", "css", "go", "html", "javascript", "json", "markdown", "python", "rust", "typescript", "yaml", "jsx", "tsx"]);
|
|
33
|
+
const tokensCache = new Map<string, { size: number; tokens: SyntaxToken[][] }>();
|
|
34
|
+
let cachedChars = 0;
|
|
35
|
+
|
|
36
|
+
export function syntaxKey(colors: SyntaxColors): string {
|
|
37
|
+
return [colors.foreground, colors.foregroundMuted, colors.accent, colors.surface1, colors.statusSuccess, colors.statusWarning].join("|");
|
|
38
|
+
}
|
|
39
|
+
function tokenLines(stream: Prism.TokenStream, colors: SyntaxColors, json: boolean): SyntaxToken[][] {
|
|
40
|
+
const lines: SyntaxToken[][] = [[]];
|
|
41
|
+
function visit(value: Prism.TokenStream, color: string): void {
|
|
42
|
+
if (typeof value === "string") {
|
|
43
|
+
const parts = value.split("\n");
|
|
44
|
+
for (let index = 0; index < parts.length; index++) {
|
|
45
|
+
if (index) lines.push([]);
|
|
46
|
+
if (parts[index]) lines[lines.length - 1]!.push({ content: parts[index]!, color });
|
|
47
|
+
}
|
|
48
|
+
} else if (Array.isArray(value)) {
|
|
49
|
+
for (const child of value) visit(child, color);
|
|
50
|
+
} else {
|
|
51
|
+
const types = [value.type, ...typeof value.alias === "string" ? [value.alias] : value.alias ?? []];
|
|
52
|
+
// Accent is a control/background color in some host themes (including a
|
|
53
|
+
// dark green shared by light and dark Paseo). Keys need foreground contrast.
|
|
54
|
+
const nextColor = json && types.includes("property") ? colors.foreground
|
|
55
|
+
: json && types.includes("string") ? colors.statusSuccess ?? colors.accent
|
|
56
|
+
: json && types.some(type => ["number", "boolean", "null"].includes(type)) ? colors.statusWarning ?? colors.accent
|
|
57
|
+
: types.some((type) => ["comment", "punctuation", "operator"].includes(type))
|
|
58
|
+
? colors.foregroundMuted
|
|
59
|
+
: types.some((type) => ["string", "char", "keyword", "template-string"].includes(type))
|
|
60
|
+
? colors.accent : color;
|
|
61
|
+
visit(value.content, nextColor);
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
visit(stream, colors.foreground);
|
|
65
|
+
return lines;
|
|
66
|
+
}
|
|
67
|
+
function normalizeLanguage(language: string): string {
|
|
68
|
+
const normalized = language.toLowerCase().trim();
|
|
69
|
+
const aliases: Record<string, string> = { sh: "bash", shell: "bash", shellscript: "bash", js: "javascript", ts: "typescript", yml: "yaml", md: "markdown", py: "python", rs: "rust" };
|
|
70
|
+
return Object.prototype.hasOwnProperty.call(aliases, normalized) ? aliases[normalized]! : normalized;
|
|
71
|
+
}
|
|
72
|
+
export async function highlightCode(code: string, language: string, colors: SyntaxColors): Promise<SyntaxToken[][] | null> {
|
|
73
|
+
const lang = normalizeLanguage(language);
|
|
74
|
+
if (!code || code.length > MAX_HIGHLIGHT_CHARS || !supported.has(lang)) return null;
|
|
75
|
+
const key = syntaxKey(colors) + ":" + lang + ":" + code;
|
|
76
|
+
const cached = tokensCache.get(key);
|
|
77
|
+
if (cached) {
|
|
78
|
+
tokensCache.delete(key);
|
|
79
|
+
tokensCache.set(key, cached);
|
|
80
|
+
return cached.tokens;
|
|
81
|
+
}
|
|
82
|
+
try {
|
|
83
|
+
const tokens = tokenLines(Prism.tokenize(code, Prism.languages[lang]!), colors, lang === "json");
|
|
84
|
+
tokensCache.set(key, { size: code.length, tokens });
|
|
85
|
+
cachedChars += code.length;
|
|
86
|
+
while (tokensCache.size > 40 || cachedChars > 500_000) {
|
|
87
|
+
const oldest = tokensCache.keys().next().value!;
|
|
88
|
+
cachedChars -= tokensCache.get(oldest)!.size;
|
|
89
|
+
tokensCache.delete(oldest);
|
|
90
|
+
}
|
|
91
|
+
return tokens;
|
|
92
|
+
} catch {
|
|
93
|
+
return null;
|
|
94
|
+
}
|
|
95
|
+
}
|
|
96
|
+
export function useSyntaxTokens(code: string, language: string, colors: SyntaxColors): SyntaxToken[][] | null {
|
|
97
|
+
const key = syntaxKey(colors);
|
|
98
|
+
const [result, setResult] = useState<{ code: string; language: string; key: string; tokens: SyntaxToken[][] | null }>();
|
|
99
|
+
useEffect(() => {
|
|
100
|
+
let cancelled = false;
|
|
101
|
+
void highlightCode(code, language, colors).then((tokens) => {
|
|
102
|
+
if (!cancelled) setResult({ code, language, key, tokens });
|
|
103
|
+
});
|
|
104
|
+
return () => { cancelled = true; };
|
|
105
|
+
}, [code, language, key]);
|
|
106
|
+
// Never display tokens from a previous streaming value or theme, even for one render.
|
|
107
|
+
return result?.code === code && result.language === language && result.key === key ? result.tokens : null;
|
|
108
|
+
}
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
import type { PluginTimelineTransformerContribution } from "@getpaseo/plugin/client";
|
|
2
|
+
import { createToolCallData, TOOL_CALL_RENDERER_KIND, TOOL_CALL_RENDERER_VERSION,
|
|
3
|
+
REASONING_RENDERER_KIND, REASONING_RENDERER_VERSION } from "../shared/timeline";
|
|
4
|
+
|
|
5
|
+
// Density-only exception: retain exact thought text, without Markdown rewriting.
|
|
6
|
+
export const transformReasoning: PluginTimelineTransformerContribution<"reasoning">["transform"] = ({ item, phase }) => ({
|
|
7
|
+
items: [{ type: "plugin", kind: REASONING_RENDERER_KIND, version: REASONING_RENDERER_VERSION,
|
|
8
|
+
data: { text: item.text, phase } }],
|
|
9
|
+
});
|
|
10
|
+
|
|
11
|
+
export const transformToolCall: PluginTimelineTransformerContribution<"tool_call">["transform"] = ({ item }) => {
|
|
12
|
+
// 0.9/0.10 beta transform before native plan suppression/presentation. Keep approval
|
|
13
|
+
// tools and structured plans native, including rejected/completed plans.
|
|
14
|
+
if (item.name === "ExitPlanMode" || item.name === "plan_approval" || item.detail?.type === "plan") return undefined;
|
|
15
|
+
// This exact shape is a native SpeakMessage, not an ordinary tool card.
|
|
16
|
+
if (item.name === "speak" && item.detail?.type === "unknown"
|
|
17
|
+
&& typeof item.detail.input === "string" && item.detail.input.trim()) return undefined;
|
|
18
|
+
return {
|
|
19
|
+
items: [{
|
|
20
|
+
type: "plugin",
|
|
21
|
+
kind: TOOL_CALL_RENDERER_KIND,
|
|
22
|
+
version: TOOL_CALL_RENDERER_VERSION,
|
|
23
|
+
data: createToolCallData(item),
|
|
24
|
+
}],
|
|
25
|
+
};
|
|
26
|
+
};
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
package/index.client.tsx
ADDED
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
import type { PluginClientContext } from "@getpaseo/plugin/client";
|
|
2
|
+
import { ReasoningActivity, ToolActivity } from "./client/activity";
|
|
3
|
+
import { transformReasoning, transformToolCall } from "./client/transform";
|
|
4
|
+
import { TOOL_CALL_RENDERER_KIND, TOOL_CALL_RENDERER_VERSION, toolCallItemDataSchema,
|
|
5
|
+
REASONING_RENDERER_KIND, REASONING_RENDERER_VERSION, reasoningItemDataSchema } from "./shared/timeline";
|
|
6
|
+
|
|
7
|
+
export default function contribute(client: PluginClientContext) {
|
|
8
|
+
// Paseo 0.8 to 0.10 expose no public display-mode/group context, so cards bypass Summary
|
|
9
|
+
// grouping. Never change the host preference or guess it from private app state; the user
|
|
10
|
+
// switches this client to native tool rows instead. Reasoning rows never join tool groups,
|
|
11
|
+
// so the Thinking adapter stays. The choice is client-local and resets on reload.
|
|
12
|
+
let removeToolCards: (() => void) | undefined;
|
|
13
|
+
function useCards() {
|
|
14
|
+
removeToolCards ??= client.addTimelineTransformer({ id: "tool-calls", query: { itemType: "tool_call" }, transform: transformToolCall });
|
|
15
|
+
}
|
|
16
|
+
function useNativeRows() {
|
|
17
|
+
removeToolCards?.();
|
|
18
|
+
removeToolCards = undefined;
|
|
19
|
+
}
|
|
20
|
+
const remove = [
|
|
21
|
+
client.addTimelineRenderer({ kind: TOOL_CALL_RENDERER_KIND, version: TOOL_CALL_RENDERER_VERSION, schema: toolCallItemDataSchema, Component: ToolActivity }),
|
|
22
|
+
client.addTimelineTransformer({ id: "reasoning", query: { itemType: "reasoning" }, transform: transformReasoning }),
|
|
23
|
+
client.addTimelineRenderer({ kind: REASONING_RENDERER_KIND, version: REASONING_RENDERER_VERSION, schema: reasoningItemDataSchema, Component: ReasoningActivity }),
|
|
24
|
+
client.addCommandCenterItem({
|
|
25
|
+
id: "native-tool-rows", title: "Activity: use native tool rows (for Summary)", icon: "List", context: "global",
|
|
26
|
+
keywords: ["activity", "summary", "tool", "native"], onSelect: useNativeRows,
|
|
27
|
+
}),
|
|
28
|
+
client.addCommandCenterItem({
|
|
29
|
+
id: "tool-cards", title: "Activity: use tool cards (Full detail)", icon: "LayoutList", context: "global",
|
|
30
|
+
keywords: ["activity", "full detail", "tool", "cards"], onSelect: useCards,
|
|
31
|
+
}),
|
|
32
|
+
];
|
|
33
|
+
useCards();
|
|
34
|
+
return () => { useNativeRows(); for (const cleanup of remove.reverse()) cleanup(); };
|
|
35
|
+
}
|
|
@@ -0,0 +1,75 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "paseo-readable-agent-activity",
|
|
3
|
+
"version": "0.1.0-beta.11",
|
|
4
|
+
"lockfileVersion": 3,
|
|
5
|
+
"requires": true,
|
|
6
|
+
"packages": {
|
|
7
|
+
"": {
|
|
8
|
+
"name": "paseo-readable-agent-activity",
|
|
9
|
+
"version": "0.1.0-beta.11",
|
|
10
|
+
"license": "MIT",
|
|
11
|
+
"dependencies": {
|
|
12
|
+
"@getpaseo/protocol": "0.9.2",
|
|
13
|
+
"prism-react-renderer": "2.4.1"
|
|
14
|
+
}
|
|
15
|
+
},
|
|
16
|
+
"node_modules/@getpaseo/protocol": {
|
|
17
|
+
"version": "0.9.2",
|
|
18
|
+
"resolved": "https://registry.npmjs.org/@getpaseo/protocol/-/protocol-0.9.2.tgz",
|
|
19
|
+
"integrity": "sha512-CGfyfqOVNxETLCgVhQGeaP6PtP17FvP5wBuRxmVQ+WIw3F/qCNju/a5aSDF+vbh+62Qf2qtha9MpQB9DrVJehA==",
|
|
20
|
+
"dependencies": {
|
|
21
|
+
"semver": "^7.8.5",
|
|
22
|
+
"zod": "^4.4.3"
|
|
23
|
+
}
|
|
24
|
+
},
|
|
25
|
+
"node_modules/@types/prismjs": {
|
|
26
|
+
"version": "1.26.6",
|
|
27
|
+
"resolved": "https://registry.npmjs.org/@types/prismjs/-/prismjs-1.26.6.tgz",
|
|
28
|
+
"integrity": "sha512-vqlvI7qlMvcCBbVe0AKAb4f97//Hy0EBTaiW8AalRnG/xAN5zOiWWyrNqNXeq8+KAuvRewjCVY1+IPxk4RdNYw==",
|
|
29
|
+
"license": "MIT"
|
|
30
|
+
},
|
|
31
|
+
"node_modules/clsx": {
|
|
32
|
+
"version": "2.1.1",
|
|
33
|
+
"resolved": "https://registry.npmjs.org/clsx/-/clsx-2.1.1.tgz",
|
|
34
|
+
"integrity": "sha512-eYm0QWBtUrBWZWG0d386OGAw16Z995PiOVo2B7bjWSbHedGl5e0ZWaq65kOGgUSNesEIDkB9ISbTg/JK9dhCZA==",
|
|
35
|
+
"license": "MIT",
|
|
36
|
+
"engines": {
|
|
37
|
+
"node": ">=6"
|
|
38
|
+
}
|
|
39
|
+
},
|
|
40
|
+
"node_modules/prism-react-renderer": {
|
|
41
|
+
"version": "2.4.1",
|
|
42
|
+
"resolved": "https://registry.npmjs.org/prism-react-renderer/-/prism-react-renderer-2.4.1.tgz",
|
|
43
|
+
"integrity": "sha512-ey8Ls/+Di31eqzUxC46h8MksNuGx/n0AAC8uKpwFau4RPDYLuE3EXTp8N8G2vX2N7UC/+IXeNUnlWBGGcAG+Ig==",
|
|
44
|
+
"license": "MIT",
|
|
45
|
+
"dependencies": {
|
|
46
|
+
"@types/prismjs": "^1.26.0",
|
|
47
|
+
"clsx": "^2.0.0"
|
|
48
|
+
},
|
|
49
|
+
"peerDependencies": {
|
|
50
|
+
"react": ">=16.0.0"
|
|
51
|
+
}
|
|
52
|
+
},
|
|
53
|
+
"node_modules/semver": {
|
|
54
|
+
"version": "7.8.5",
|
|
55
|
+
"resolved": "https://registry.npmjs.org/semver/-/semver-7.8.5.tgz",
|
|
56
|
+
"integrity": "sha512-Y7/KDsb8LjooZpwaqGyulO6DQlksgCncchHGk+sZIY4SBvUocMBEFH5Ur1fI4dV+Jvl0w6cjvucaIi40puRioA==",
|
|
57
|
+
"license": "ISC",
|
|
58
|
+
"bin": {
|
|
59
|
+
"semver": "bin/semver.js"
|
|
60
|
+
},
|
|
61
|
+
"engines": {
|
|
62
|
+
"node": ">=10"
|
|
63
|
+
}
|
|
64
|
+
},
|
|
65
|
+
"node_modules/zod": {
|
|
66
|
+
"version": "4.6.1",
|
|
67
|
+
"resolved": "https://registry.npmjs.org/zod/-/zod-4.6.1.tgz",
|
|
68
|
+
"integrity": "sha512-341aRWQsve0rvronKNTqZpjmzdbUDlFuzHaI/XLg/Ej82qffDJRRfBTCuv7+9q/rMjB6LSLyEBnW4InJeMtt/Q==",
|
|
69
|
+
"license": "MIT",
|
|
70
|
+
"funding": {
|
|
71
|
+
"url": "https://github.com/sponsors/colinhacks"
|
|
72
|
+
}
|
|
73
|
+
}
|
|
74
|
+
}
|
|
75
|
+
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,45 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "paseo-readable-agent-activity",
|
|
3
|
-
"version": "0.
|
|
4
|
-
"
|
|
5
|
-
"
|
|
6
|
-
|
|
3
|
+
"version": "0.1.0-beta.11",
|
|
4
|
+
"description": "Formatted and highlighted tool-call JSON, twenty-line previews, tool icons and the main argument on collapsed cards, for Paseo 0.8 or later. Tool cards need Full detail.",
|
|
5
|
+
"repository": {
|
|
6
|
+
"type": "git",
|
|
7
|
+
"url": "https://github.com/geoqiao/paseo-stuff.git",
|
|
8
|
+
"directory": "plugins/agent-activity"
|
|
9
|
+
},
|
|
10
|
+
"homepage": "https://github.com/geoqiao/paseo-stuff/tree/main/plugins/agent-activity#readme",
|
|
11
|
+
"bugs": {
|
|
12
|
+
"url": "https://github.com/geoqiao/paseo-stuff/issues"
|
|
13
|
+
},
|
|
14
|
+
"keywords": [
|
|
15
|
+
"paseo",
|
|
16
|
+
"paseo-plugin",
|
|
17
|
+
"tool-calls",
|
|
18
|
+
"json",
|
|
19
|
+
"readability"
|
|
20
|
+
],
|
|
21
|
+
"files": [
|
|
22
|
+
"paseo-plugin.json",
|
|
23
|
+
"OVERVIEW.md",
|
|
24
|
+
"index.client.tsx",
|
|
25
|
+
"client/activity.tsx",
|
|
26
|
+
"client/highlight.ts",
|
|
27
|
+
"client/transform.ts",
|
|
28
|
+
"shared/details.ts",
|
|
29
|
+
"shared/presentation.ts",
|
|
30
|
+
"shared/timeline.ts",
|
|
31
|
+
"README.md",
|
|
32
|
+
"LICENSE",
|
|
33
|
+
"CHANGELOG.md",
|
|
34
|
+
"UPSTREAM.md",
|
|
35
|
+
"images/",
|
|
36
|
+
"package.json",
|
|
37
|
+
"npm-shrinkwrap.json"
|
|
38
|
+
],
|
|
39
|
+
"license": "MIT",
|
|
40
|
+
"type": "module",
|
|
41
|
+
"dependencies": {
|
|
42
|
+
"@getpaseo/protocol": "0.9.2",
|
|
43
|
+
"prism-react-renderer": "2.4.1"
|
|
44
|
+
}
|
|
45
|
+
}
|
|
@@ -0,0 +1,144 @@
|
|
|
1
|
+
import type { ToolCallItemData } from "./timeline";
|
|
2
|
+
|
|
3
|
+
export const PREVIEW_LINES = 20;
|
|
4
|
+
export const MAX_FORMAT_CHARS = 1_000_000;
|
|
5
|
+
|
|
6
|
+
export interface DisplayValue {
|
|
7
|
+
text: string;
|
|
8
|
+
language: string;
|
|
9
|
+
}
|
|
10
|
+
export interface DetailSection {
|
|
11
|
+
label: "Input" | "Output";
|
|
12
|
+
value: unknown;
|
|
13
|
+
language?: string;
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
function record(value: unknown): Record<string, unknown> | undefined {
|
|
17
|
+
return value !== null && typeof value === "object" && !Array.isArray(value)
|
|
18
|
+
? value as Record<string, unknown> : undefined;
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
/** Whitespace only: retain numeric lexemes, duplicate keys, order and escapes. */
|
|
22
|
+
export function prettyJson(source: string): string | null {
|
|
23
|
+
if (!source.trim() || source.length > MAX_FORMAT_CHARS) return null;
|
|
24
|
+
try { JSON.parse(source); } catch { return null; }
|
|
25
|
+
const tokens = source.match(/"(?:\\[\s\S]|[^"\\])*"|[{}[\],:]|[^\s{}[\],:]+/g) ?? [];
|
|
26
|
+
const out: string[] = [];
|
|
27
|
+
let depth = 0, length = 0;
|
|
28
|
+
const push = (text: string) => { out.push(text); length += text.length; };
|
|
29
|
+
const line = () => push("\n" + " ".repeat(Math.min(depth, 40)));
|
|
30
|
+
for (let index = 0; index < tokens.length; index++) {
|
|
31
|
+
const token = tokens[index]!;
|
|
32
|
+
if (token === "{" || token === "[") {
|
|
33
|
+
push(token); depth++;
|
|
34
|
+
if (tokens[index + 1] !== "}" && tokens[index + 1] !== "]") line();
|
|
35
|
+
} else if (token === "}" || token === "]") {
|
|
36
|
+
depth--;
|
|
37
|
+
if (tokens[index - 1] !== "{" && tokens[index - 1] !== "[") line();
|
|
38
|
+
push(token);
|
|
39
|
+
} else if (token === ",") { push(token); line(); }
|
|
40
|
+
else if (token === ":") push(": ");
|
|
41
|
+
else push(token);
|
|
42
|
+
// Bound indentation amplification; fall back to the intact original.
|
|
43
|
+
if (length > MAX_FORMAT_CHARS) return null;
|
|
44
|
+
}
|
|
45
|
+
return out.join("");
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
export function presentValue(value: unknown, language = "text"): DisplayValue {
|
|
49
|
+
if (value === undefined) return { text: "", language };
|
|
50
|
+
if (typeof value === "string") {
|
|
51
|
+
const formatted = prettyJson(value);
|
|
52
|
+
return { text: formatted ?? value, language: formatted !== null ? "json" : language };
|
|
53
|
+
}
|
|
54
|
+
try {
|
|
55
|
+
// Reuse the bounded whitespace formatter: JSON.stringify(value, null, 2)
|
|
56
|
+
// can amplify deeply nested objects into unbounded indentation.
|
|
57
|
+
const source = JSON.stringify(value) ?? "";
|
|
58
|
+
return { text: prettyJson(source) ?? source, language: "json" };
|
|
59
|
+
} catch {
|
|
60
|
+
return { text: String(value), language: "text" };
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
/** Bound the source prefix before layout; the renderer also bounds wrapped lines. */
|
|
65
|
+
export function previewText(text: string): { text: string; truncated: boolean } {
|
|
66
|
+
let end = -1;
|
|
67
|
+
for (let line = 0; line < PREVIEW_LINES; line++) {
|
|
68
|
+
end = text.indexOf("\n", end + 1);
|
|
69
|
+
if (end === -1) return { text, truncated: false };
|
|
70
|
+
}
|
|
71
|
+
return { text: text.slice(0, end), truncated: true };
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
function onlyKeys(value: Record<string, unknown>, keys: string[]): boolean {
|
|
75
|
+
const actual = Object.keys(value);
|
|
76
|
+
return actual.length === keys.length && keys.every(key => Object.hasOwn(value, key));
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
/** Unwrap only a pure single-text transport. Never drop metadata or attachments.
|
|
80
|
+
* Serialized JSON strings stay strings: parsing/re-serializing could lose keys/numbers.
|
|
81
|
+
*/
|
|
82
|
+
function outputValue(value: unknown): unknown {
|
|
83
|
+
const envelope = record(value);
|
|
84
|
+
if (!envelope || !onlyKeys(envelope, ["content"]) || !Array.isArray(envelope.content)
|
|
85
|
+
|| envelope.content.length !== 1) return value;
|
|
86
|
+
let block = record(envelope.content[0]);
|
|
87
|
+
if (block?.type === "content" && onlyKeys(block, ["type", "content"])) block = record(block.content);
|
|
88
|
+
return block?.type === "text" && onlyKeys(block, ["type", "text"]) && typeof block.text === "string"
|
|
89
|
+
? block.text : value;
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
function inputSection(value: unknown, tool: string): DetailSection {
|
|
93
|
+
const name = tool.trim().toLowerCase().replace(/^(?:functions|tools)\./, "");
|
|
94
|
+
const codeTool = ["exec", "mcpscript", "mcp_script"].includes(name);
|
|
95
|
+
const input = record(value);
|
|
96
|
+
// A source-only wrapper is transparent; extra parameters remain structured JSON.
|
|
97
|
+
if (codeTool && input && onlyKeys(input, ["code"]) && typeof input.code === "string"
|
|
98
|
+
&& input.code.trim()) return { label: "Input", value: input.code, language: "javascript" };
|
|
99
|
+
return { label: "Input", value, ...(codeTool && typeof value === "string" ? { language: "javascript" } : {}) };
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
function without(value: Record<string, unknown>, keys: string[]): Record<string, unknown> {
|
|
103
|
+
return Object.fromEntries(Object.entries(value).filter(([key]) => !keys.includes(key)));
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
/** Thin host-detail adaptation, not tool execution or semantic result interpretation. */
|
|
107
|
+
export function detailSections(data: ToolCallItemData): DetailSection[] {
|
|
108
|
+
const detail = record(data.detail);
|
|
109
|
+
let input: unknown;
|
|
110
|
+
let output: unknown = data.detail;
|
|
111
|
+
if (detail) {
|
|
112
|
+
switch (detail.type) {
|
|
113
|
+
case "unknown": {
|
|
114
|
+
input = detail.input;
|
|
115
|
+
const extra = without(detail, ["type", "input", "output"]);
|
|
116
|
+
output = Object.keys(extra).length ? { ...extra, output: detail.output } : outputValue(detail.output);
|
|
117
|
+
break;
|
|
118
|
+
}
|
|
119
|
+
case "shell": {
|
|
120
|
+
input = { command: detail.command, ...(Object.hasOwn(detail, "cwd") ? { cwd: detail.cwd } : {}) };
|
|
121
|
+
const result = without(detail, ["type", "command", "cwd"]);
|
|
122
|
+
output = onlyKeys(result, ["output"]) ? result.output : result;
|
|
123
|
+
break;
|
|
124
|
+
}
|
|
125
|
+
case "read":
|
|
126
|
+
input = without(detail, ["type", "content"]);
|
|
127
|
+
output = detail.content;
|
|
128
|
+
break;
|
|
129
|
+
case "write":
|
|
130
|
+
case "edit":
|
|
131
|
+
input = without(detail, ["type"]);
|
|
132
|
+
output = undefined;
|
|
133
|
+
break;
|
|
134
|
+
case "plain_text":
|
|
135
|
+
case "plan": {
|
|
136
|
+
const result = without(detail, ["type"]);
|
|
137
|
+
output = onlyKeys(result, ["text"]) ? result.text : result;
|
|
138
|
+
break;
|
|
139
|
+
}
|
|
140
|
+
}
|
|
141
|
+
}
|
|
142
|
+
if (data.error !== undefined) output = { ...(output !== undefined ? { output } : {}), error: data.error };
|
|
143
|
+
return [inputSection(input, data.name), { label: "Output", value: output }];
|
|
144
|
+
}
|
|
@@ -0,0 +1,141 @@
|
|
|
1
|
+
import { getPaseoToolLeafName } from "@getpaseo/protocol/tool-name-normalization";
|
|
2
|
+
|
|
3
|
+
export const PASEO_TOOL_ICONS: Readonly<Record<string, string>> = {
|
|
4
|
+
speak: "MicVocal",
|
|
5
|
+
create_workspace: "FolderPlus",
|
|
6
|
+
list_workspaces: "Folders",
|
|
7
|
+
archive_workspace: "Archive",
|
|
8
|
+
create_agent: "Bot",
|
|
9
|
+
send_agent_prompt: "Send",
|
|
10
|
+
get_agent_status: "Activity",
|
|
11
|
+
list_agents: "Users",
|
|
12
|
+
cancel_agent: "CircleStop",
|
|
13
|
+
archive_agent: "Archive",
|
|
14
|
+
kill_agent: "CircleX",
|
|
15
|
+
update_agent: "Settings2",
|
|
16
|
+
rename_workspace: "Pencil",
|
|
17
|
+
list_workspace_scripts: "ListTree",
|
|
18
|
+
start_workspace_script: "Play",
|
|
19
|
+
stop_workspace_script: "Square",
|
|
20
|
+
list_terminals: "SquareTerminal",
|
|
21
|
+
create_terminal: "SquareTerminal",
|
|
22
|
+
kill_terminal: "CircleX",
|
|
23
|
+
capture_terminal: "ScrollText",
|
|
24
|
+
send_terminal_keys: "Keyboard",
|
|
25
|
+
create_schedule: "CalendarClock",
|
|
26
|
+
create_heartbeat: "HeartPulse",
|
|
27
|
+
delete_heartbeat: "Trash2",
|
|
28
|
+
list_schedules: "CalendarDays",
|
|
29
|
+
inspect_schedule: "CalendarSearch",
|
|
30
|
+
pause_schedule: "Pause",
|
|
31
|
+
resume_schedule: "Play",
|
|
32
|
+
delete_schedule: "Trash2",
|
|
33
|
+
update_schedule: "CalendarCog",
|
|
34
|
+
schedule_logs: "ScrollText",
|
|
35
|
+
run_schedule_once: "CalendarCheck",
|
|
36
|
+
list_providers: "Network",
|
|
37
|
+
list_models: "Cpu",
|
|
38
|
+
list_profiles: "ContactRound",
|
|
39
|
+
inspect_provider: "ScanSearch",
|
|
40
|
+
get_agent_activity: "Activity",
|
|
41
|
+
set_agent_mode: "SlidersHorizontal",
|
|
42
|
+
list_pending_permissions: "ShieldAlert",
|
|
43
|
+
respond_to_permission: "ShieldCheck",
|
|
44
|
+
browser_list_tabs: "PanelsTopLeft",
|
|
45
|
+
browser_new_tab: "Globe2",
|
|
46
|
+
browser_snapshot: "Scan",
|
|
47
|
+
browser_click: "MousePointer2",
|
|
48
|
+
browser_fill: "TextCursorInput",
|
|
49
|
+
browser_wait: "Timer",
|
|
50
|
+
browser_type: "Keyboard",
|
|
51
|
+
browser_keypress: "KeyRound",
|
|
52
|
+
browser_navigate: "Navigation",
|
|
53
|
+
browser_back: "ArrowLeft",
|
|
54
|
+
browser_forward: "ArrowRight",
|
|
55
|
+
browser_reload: "RefreshCw",
|
|
56
|
+
browser_screenshot: "Camera",
|
|
57
|
+
browser_upload: "Upload",
|
|
58
|
+
browser_hover: "Hand",
|
|
59
|
+
browser_select: "ListFilter",
|
|
60
|
+
browser_drag: "Move",
|
|
61
|
+
browser_logs: "ScrollText",
|
|
62
|
+
browser_evaluate: "Braces",
|
|
63
|
+
browser_scroll: "Scroll",
|
|
64
|
+
browser_resize: "Maximize2",
|
|
65
|
+
browser_close_tab: "X",
|
|
66
|
+
};
|
|
67
|
+
|
|
68
|
+
|
|
69
|
+
const TOOL_ICONS: Readonly<Record<string, string>> = {
|
|
70
|
+
exec: "Code", mcpscript: "Code", mcp_script: "Code",
|
|
71
|
+
bash: "Terminal", shell: "Terminal", exec_command: "Terminal",
|
|
72
|
+
run_command: "Terminal", run_shell_command: "Terminal", write_stdin: "Keyboard",
|
|
73
|
+
wait: "Clock", read: "Eye", read_file: "Eye", write: "FilePen", write_file: "FilePen",
|
|
74
|
+
edit: "Pencil", edit_file: "Pencil", apply_patch: "FileDiff", view_image: "Image",
|
|
75
|
+
find_roots: "Monitor", observe_ui: "Eye", search_ui: "Search", inspect_ui: "Eye",
|
|
76
|
+
expand_ui: "List", read_text: "FileText", act_ui: "MousePointer", wait_for: "Clock",
|
|
77
|
+
launch_browser: "Globe", navigate_browser: "Globe", evaluate_browser: "Code",
|
|
78
|
+
fetch_content: "Globe", web_search: "Search", source_check: "SearchCheck",
|
|
79
|
+
get_search_content: "FileSearch", ask_user: "MessageSquare", mcp: "Plug",
|
|
80
|
+
speak: "MicVocal", task: "Bot", thinking: "Brain",
|
|
81
|
+
actions_get: "Workflow", actions_list: "ListChecks", actions_run_trigger: "PlayCircle",
|
|
82
|
+
get_file_contents: "FileCode2", get_job_logs: "ScrollText", pull_request_read: "GitPullRequest",
|
|
83
|
+
search_code: "Code2", search_repositories: "BookMarked",
|
|
84
|
+
web_search_exa: "Search", web_fetch_exa: "Globe", exa_agent_run: "Bot",
|
|
85
|
+
exa_read_agent_research_guide: "BookOpen", exa_read_agent_schema_templates: "Braces",
|
|
86
|
+
};
|
|
87
|
+
const DETAIL_ICONS: Readonly<Record<string, string>> = {
|
|
88
|
+
shell: "SquareTerminal", read: "Eye", write: "FilePen", edit: "Pencil",
|
|
89
|
+
worktree_setup: "GitBranch", search: "Search", fetch: "Globe",
|
|
90
|
+
sub_agent: "Bot", plain_text: "MessageSquare", plan: "ListChecks",
|
|
91
|
+
};
|
|
92
|
+
|
|
93
|
+
function lookup(map: Readonly<Record<string, string>>, key: string): string | undefined {
|
|
94
|
+
return Object.hasOwn(map, key) ? map[key] : undefined;
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
/** Name/category mapping only. Never inspect arguments or derive a business summary. */
|
|
98
|
+
export function toolIcon(toolName: string, detail?: unknown): string {
|
|
99
|
+
const normalized = toolName.trim().toLowerCase().replace(/^(?:functions|tools)\./, "");
|
|
100
|
+
const paseo = getPaseoToolLeafName(normalized) ?? normalized.match(/^(?:mcp_)?paseo_(.+)$/)?.[1];
|
|
101
|
+
if (paseo) return lookup(PASEO_TOOL_ICONS, paseo) ?? "Sparkles";
|
|
102
|
+
const leaf = normalized.split(/__|[.:/]/).at(-1) ?? normalized;
|
|
103
|
+
const icon = lookup(TOOL_ICONS, leaf) ?? lookup(TOOL_ICONS, leaf.replace(/^github_/, ""));
|
|
104
|
+
// Native typed shell "exec" is not Code mode.
|
|
105
|
+
const type = detail !== null && typeof detail === "object" && "type" in detail ? detail.type : undefined;
|
|
106
|
+
if (typeof type === "string" && type !== "unknown") return lookup(DETAIL_ICONS, type) ?? icon ?? "Wrench";
|
|
107
|
+
return icon ?? (/(?:^|__)mcp(?:__|$)/.test(normalized) ? "Plug" : "Wrench");
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
const DETAIL_ARGUMENTS: Readonly<Record<string, string>> = {
|
|
111
|
+
shell: "command", read: "filePath", write: "filePath", edit: "filePath",
|
|
112
|
+
search: "query", fetch: "url", sub_agent: "description", worktree_setup: "branchName",
|
|
113
|
+
};
|
|
114
|
+
const INPUT_ARGUMENTS = ["command", "cmd", "file_path", "filePath", "path", "pattern", "query", "url"];
|
|
115
|
+
const ARGUMENT_CHARS = 200;
|
|
116
|
+
|
|
117
|
+
/** The call's own main argument for the collapsed header: one literal value, first
|
|
118
|
+
* line only, from a bounded prefix. Never a generated or combined summary. */
|
|
119
|
+
export function toolArgument(detail: unknown): string | undefined {
|
|
120
|
+
if (detail === null || typeof detail !== "object" || Array.isArray(detail)) return undefined;
|
|
121
|
+
const fields = detail as Record<string, unknown>;
|
|
122
|
+
const type = typeof fields.type === "string" ? fields.type : "";
|
|
123
|
+
let value: unknown;
|
|
124
|
+
if (type === "unknown") {
|
|
125
|
+
const input = fields.input;
|
|
126
|
+
if (input !== null && typeof input === "object" && !Array.isArray(input)) {
|
|
127
|
+
const key = INPUT_ARGUMENTS.find(name => Object.hasOwn(input, name)
|
|
128
|
+
&& typeof (input as Record<string, unknown>)[name] === "string");
|
|
129
|
+
if (key) value = (input as Record<string, unknown>)[key];
|
|
130
|
+
}
|
|
131
|
+
} else {
|
|
132
|
+
const key = lookup(DETAIL_ARGUMENTS, type);
|
|
133
|
+
if (key && Object.hasOwn(fields, key)) value = fields[key];
|
|
134
|
+
}
|
|
135
|
+
if (typeof value !== "string") return undefined;
|
|
136
|
+
const prefix = value.slice(0, ARGUMENT_CHARS).trimStart();
|
|
137
|
+
const end = prefix.search(/[\r\n]/);
|
|
138
|
+
const line = (end === -1 ? prefix : prefix.slice(0, end)).trimEnd();
|
|
139
|
+
if (!line) return undefined;
|
|
140
|
+
return end !== -1 || value.length > ARGUMENT_CHARS ? line + "…" : line;
|
|
141
|
+
}
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
import type { AgentTimelineItem, JsonValue } from "@getpaseo/protocol/agent-types";
|
|
2
|
+
import { z } from "zod";
|
|
3
|
+
import { toolIcon } from "./presentation";
|
|
4
|
+
|
|
5
|
+
export const TOOL_CALL_RENDERER_KIND = "colorful-tool-call";
|
|
6
|
+
export const TOOL_CALL_RENDERER_VERSION = 1;
|
|
7
|
+
export const REASONING_RENDERER_KIND = "colorful-reasoning";
|
|
8
|
+
export const REASONING_RENDERER_VERSION = 1;
|
|
9
|
+
|
|
10
|
+
export const reasoningItemDataSchema = z.object({
|
|
11
|
+
text: z.string(),
|
|
12
|
+
phase: z.enum(["streaming", "complete"]),
|
|
13
|
+
});
|
|
14
|
+
export type ReasoningItemData = z.output<typeof reasoningItemDataSchema>;
|
|
15
|
+
|
|
16
|
+
export const toolCallItemDataSchema = z.object({
|
|
17
|
+
name: z.string(),
|
|
18
|
+
status: z.enum(["running", "completed", "failed", "canceled"]),
|
|
19
|
+
detail: z.json(),
|
|
20
|
+
error: z.json().optional(),
|
|
21
|
+
presentation: z.object({ icon: z.string() }),
|
|
22
|
+
});
|
|
23
|
+
export type ToolCallItemData = z.output<typeof toolCallItemDataSchema>;
|
|
24
|
+
|
|
25
|
+
function jsonValue(value: unknown): JsonValue {
|
|
26
|
+
try {
|
|
27
|
+
const source = JSON.stringify(value);
|
|
28
|
+
return source === undefined ? null : JSON.parse(source) as JsonValue;
|
|
29
|
+
} catch { return null; }
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
export function createToolCallData(
|
|
33
|
+
item: Extract<AgentTimelineItem, { type: "tool_call" }>,
|
|
34
|
+
): ToolCallItemData {
|
|
35
|
+
return {
|
|
36
|
+
name: item.name,
|
|
37
|
+
status: item.status,
|
|
38
|
+
detail: jsonValue(item.detail),
|
|
39
|
+
...(item.error != null ? { error: jsonValue(item.error) } : {}),
|
|
40
|
+
presentation: { icon: toolIcon(item.name, item.detail) },
|
|
41
|
+
};
|
|
42
|
+
}
|