@astrosheep/square 0.4.6 → 0.4.8
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/dist/card-format.d.ts +2 -0
- package/dist/card-format.js +2 -0
- package/dist/claude-mcp-context.d.ts +0 -1
- package/dist/claude-mcp-context.js +0 -10
- package/dist/claude-mod.d.ts +2 -0
- package/dist/claude-mod.js +211 -0
- package/dist/cli/context.d.ts +2 -2
- package/dist/cli/context.js +14 -16
- package/dist/cli/express-presentation.js +12 -53
- package/dist/cli/json.js +2 -2
- package/dist/cli/observation-commands.d.ts +2 -7
- package/dist/cli/observation-commands.js +31 -97
- package/dist/cli/registry.js +3 -6
- package/dist/cli/square-commands.d.ts +2 -3
- package/dist/cli/square-commands.js +12 -22
- package/dist/decisions.js +5 -4
- package/dist/harness-claude.js +3 -3
- package/dist/help.js +9 -16
- package/dist/index.d.ts +1 -1
- package/dist/index.js +1 -1
- package/dist/mcp.d.ts +1 -0
- package/dist/mcp.js +7 -18
- package/dist/model.d.ts +1 -1
- package/dist/neutral.d.ts +15 -3
- package/dist/notifications.d.ts +1 -3
- package/dist/notifications.js +21 -23
- package/dist/operation-control.d.ts +0 -1
- package/dist/operation-control.js +0 -11
- package/dist/operation-presentation.d.ts +28 -21
- package/dist/operation-presentation.js +90 -87
- package/dist/packages/agent-delivery/src/index.d.ts +4 -9
- package/dist/packages/agent-delivery/src/index.js +0 -9
- package/dist/packages/agent-delivery/src/local-endpoint.d.ts +2 -12
- package/dist/packages/agent-delivery/src/local-endpoint.js +4 -48
- package/dist/packages/agent-delivery/src/pi-client.js +0 -2
- package/dist/packages/agent-delivery/src/types.d.ts +3 -37
- package/dist/participant-identity.d.ts +0 -2
- package/dist/participant-identity.js +0 -13
- package/dist/plugins/paseo/shared/card-format.d.ts +6 -0
- package/dist/plugins/paseo/shared/card-format.js +38 -0
- package/dist/plugins/paseo/shared/card-presentation.d.ts +3 -1
- package/dist/plugins/paseo/shared/card-presentation.js +3 -1
- package/dist/presentation.d.ts +5 -22
- package/dist/presentation.js +25 -58
- package/dist/runtime.d.ts +6 -6
- package/dist/runtime.js +5 -15
- package/dist/sqlite-square.d.ts +4 -5
- package/dist/sqlite-square.js +29 -32
- package/dist/square-api.d.ts +1 -1
- package/dist/square-api.js +1 -1
- package/dist/square-facade.d.ts +5 -14
- package/dist/square-location.d.ts +0 -1
- package/dist/square-posting.d.ts +4 -4
- package/dist/square-posting.js +14 -16
- package/dist/square-tools.d.ts +4 -17
- package/dist/square-tools.js +52 -119
- package/dist/square-values.d.ts +1 -1
- package/dist/square-values.js +1 -1
- package/dist/time.d.ts +1 -0
- package/dist/time.js +10 -0
- package/dist/views.d.ts +1 -1
- package/dist/views.js +2 -2
- package/dist/wake-transports.d.ts +2 -2
- package/dist/wake-transports.js +7 -9
- package/dist/wakes.js +1 -1
- package/examples/symmetric-domains.mjs +1 -2
- package/guides/catch-cold-path-optimization.md +1 -1
- package/guides/claude-native.md +37 -7
- package/guides/delivery-primitives.md +2 -1
- package/guides/mcp.md +3 -3
- package/guides/pi-native-delivery.md +10 -2
- package/guides/pi-tools.md +1 -3
- package/guides/row-storage-design.md +1 -1
- package/package.json +1 -1
- package/plugins/claude/.claude-plugin/plugin.json +3 -2
- package/plugins/claude/README.md +9 -0
- package/plugins/claude/hooks/hooks.json +1 -9
- package/plugins/claude/hooks/register.js +178 -0
- package/plugins/claude/skills/square/SKILL.md +2 -2
- package/plugins/codex/.codex-plugin/plugin.json +1 -1
- package/plugins/paseo/OVERVIEW.md +1 -1
- package/plugins/paseo/README.md +20 -23
- package/plugins/paseo/client/markdown.tsx +88 -95
- package/plugins/paseo/client/palette.ts +37 -12
- package/plugins/paseo/client/square-message.tsx +52 -76
- package/plugins/paseo/client/transform.ts +4 -6
- package/plugins/paseo/shared/card-format.ts +37 -0
- package/plugins/paseo/shared/card-presentation.ts +6 -2
- package/plugins/paseo/shared/message.ts +1 -6
- package/plugins/pi/square-pi.js +76 -60
- package/skills/square/SKILL.md +2 -2
- package/dist/claude-boundary-state.d.ts +0 -20
- package/dist/claude-boundary-state.js +0 -84
- package/dist/claude-hook.d.ts +0 -2
- package/dist/claude-hook.js +0 -232
- package/dist/packages/agent-delivery/src/claude-auth.d.ts +0 -13
- package/dist/packages/agent-delivery/src/claude-auth.js +0 -75
- package/dist/packages/agent-delivery/src/claude-native.d.ts +0 -30
- package/dist/packages/agent-delivery/src/claude-native.js +0 -75
- package/dist/packages/agent-delivery/src/claude.d.ts +0 -5
- package/dist/packages/agent-delivery/src/claude.js +0 -66
|
@@ -0,0 +1,178 @@
|
|
|
1
|
+
// The mod owns native lifecycle and effects. Its one helper owns Node/SQLite.
|
|
2
|
+
let runtime;
|
|
3
|
+
let starting;
|
|
4
|
+
|
|
5
|
+
async function rpc($, current, input) {
|
|
6
|
+
const response = await $.http.fetch(current.url, {
|
|
7
|
+
method: 'POST', headers: { 'content-type': 'application/json' }, body: JSON.stringify(input),
|
|
8
|
+
});
|
|
9
|
+
if (!response.ok) throw new Error('Square Claude helper request failed');
|
|
10
|
+
return JSON.parse(response.text);
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
async function closeRuntime($, current) {
|
|
14
|
+
if (!current || current.closed) return;
|
|
15
|
+
current.closed = true;
|
|
16
|
+
current.epoch++;
|
|
17
|
+
try { await rpc($, current, { op: 'close' }); } catch { /* The helper may already have exited. */ }
|
|
18
|
+
await current.stream.return().catch(() => undefined);
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
async function startRuntime($, sessionId) {
|
|
22
|
+
const stream = $.process.spawn({ argv: ['square', 'claude-mod'], input: JSON.stringify({ sessionId }) });
|
|
23
|
+
const current = { sessionId, stream, url: '', buffer: '', closed: false, epoch: 0, epochs: new Map(), modes: new Map([['', 'idle']]) };
|
|
24
|
+
try {
|
|
25
|
+
while (!current.url) {
|
|
26
|
+
const chunk = await stream.next();
|
|
27
|
+
if (chunk.done) throw new Error('Square Claude helper ended before startup');
|
|
28
|
+
if (chunk.value.stream !== 'stdout') { $.ui.log(chunk.value.text, { to: 'debug' }); continue; }
|
|
29
|
+
current.buffer += chunk.value.text;
|
|
30
|
+
const end = current.buffer.indexOf('\n');
|
|
31
|
+
if (end < 0) continue;
|
|
32
|
+
const ready = JSON.parse(current.buffer.slice(0, end));
|
|
33
|
+
current.buffer = current.buffer.slice(end + 1);
|
|
34
|
+
if (ready.type !== 'ready' || !/^http:\/\/127\.0\.0\.1:\d+\/[a-f0-9]{64}$/.test(ready.url)) throw new Error('Invalid Square Claude helper address');
|
|
35
|
+
current.url = ready.url;
|
|
36
|
+
}
|
|
37
|
+
// A session.start/tool hook must never hold the turn while waiting on submit.
|
|
38
|
+
$.clock.after(0, async () => readDeliveries($, current));
|
|
39
|
+
return current;
|
|
40
|
+
} catch (error) {
|
|
41
|
+
await stream.return().catch(() => undefined);
|
|
42
|
+
throw error;
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
async function ensureRuntime($) {
|
|
47
|
+
const sessionId = await $.session.id();
|
|
48
|
+
if (runtime && !runtime.closed && runtime.sessionId === sessionId) return runtime;
|
|
49
|
+
if (starting) { await starting; return ensureRuntime($); }
|
|
50
|
+
starting = (async () => {
|
|
51
|
+
await closeRuntime($, runtime);
|
|
52
|
+
const next = await startRuntime($, sessionId);
|
|
53
|
+
runtime = next;
|
|
54
|
+
return next;
|
|
55
|
+
})();
|
|
56
|
+
try { return await starting; } finally { starting = undefined; }
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
async function setMode($, current, mode, agentId) {
|
|
60
|
+
current.modes.set(agentId ?? '', mode);
|
|
61
|
+
if (mode === 'stopped' || mode === 'ended') current.epochs.set(agentId ?? '', (current.epochs.get(agentId ?? '') ?? 0) + 1);
|
|
62
|
+
return rpc($, current, { op: 'state', agentId, mode });
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
async function deliver($, current, frame) {
|
|
66
|
+
const epoch = current.epoch;
|
|
67
|
+
const key = frame.agentId ?? '';
|
|
68
|
+
const localEpoch = current.epochs.get(key);
|
|
69
|
+
const available = () => !current.closed && current.epoch === epoch && current.epochs.get(key) === localEpoch && ['busy', 'idle'].includes(current.modes.get(key));
|
|
70
|
+
if (!available() || !(await rpc($, current, { op: 'guard', token: frame.token })).ok || !available()) {
|
|
71
|
+
await rpc($, current, { op: 'settle', token: frame.token, kind: 'released' });
|
|
72
|
+
return;
|
|
73
|
+
}
|
|
74
|
+
// No await separates the last native-state fence from the chosen native call.
|
|
75
|
+
if (current.modes.get(key) === 'busy') {
|
|
76
|
+
const result = await $.session.append({ agentId: frame.agentId, message: { type: 'user', content: [{ type: 'text', text: frame.text }] } });
|
|
77
|
+
if (result.deny) {
|
|
78
|
+
await rpc($, current, { op: 'settle', token: frame.token, kind: 'failed' });
|
|
79
|
+
$.ui.log('Square context was refused: ' + result.deny, { to: 'debug' });
|
|
80
|
+
return;
|
|
81
|
+
}
|
|
82
|
+
const blocks = result.message?.content;
|
|
83
|
+
const text = Array.isArray(blocks) && blocks.every(block => block.type === 'text' && typeof block.text === 'string')
|
|
84
|
+
? blocks.map(block => block.text).join('') : undefined;
|
|
85
|
+
// Display is a separate effect, never evidence that the model read the row.
|
|
86
|
+
try { $.ui.log(frame.display); } finally {
|
|
87
|
+
await rpc($, current, { op: 'settle', token: frame.token, kind: 'append', text, uuid: result.uuid });
|
|
88
|
+
}
|
|
89
|
+
return;
|
|
90
|
+
}
|
|
91
|
+
if (frame.agentId !== undefined) {
|
|
92
|
+
await rpc($, current, { op: 'settle', token: frame.token, kind: 'released' });
|
|
93
|
+
return;
|
|
94
|
+
}
|
|
95
|
+
current.modes.set('', 'busy');
|
|
96
|
+
// Keep the plugin attribution. A Square notification is not human input.
|
|
97
|
+
const result = await $.prompt.submit({ text: frame.text });
|
|
98
|
+
if (result.drop) {
|
|
99
|
+
if (result.drop.includes('the session was stopped')) await setMode($, current, 'stopped');
|
|
100
|
+
else {
|
|
101
|
+
current.modes.set('', 'idle');
|
|
102
|
+
await rpc($, current, { op: 'settle', token: frame.token, kind: 'failed' });
|
|
103
|
+
}
|
|
104
|
+
$.ui.log('Square prompt was not submitted: ' + result.drop, { to: 'debug' });
|
|
105
|
+
return;
|
|
106
|
+
}
|
|
107
|
+
await rpc($, current, { op: 'settle', token: frame.token, kind: 'submit', text: result.text });
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
async function readDeliveries($, current) {
|
|
111
|
+
try {
|
|
112
|
+
while (!current.closed) {
|
|
113
|
+
let end;
|
|
114
|
+
while ((end = current.buffer.indexOf('\n')) >= 0) {
|
|
115
|
+
const line = current.buffer.slice(0, end);
|
|
116
|
+
current.buffer = current.buffer.slice(end + 1);
|
|
117
|
+
if (!line) continue;
|
|
118
|
+
const frame = JSON.parse(line);
|
|
119
|
+
if (frame.type === 'delivery') {
|
|
120
|
+
try { await deliver($, current, frame); }
|
|
121
|
+
catch (error) {
|
|
122
|
+
// An invoked append/submit may already have taken effect. Never try
|
|
123
|
+
// the other path, replay it, or call a fallback host on uncertainty.
|
|
124
|
+
await rpc($, current, { op: 'settle', token: frame.token, kind: 'unknown' }).catch(() => undefined);
|
|
125
|
+
$.ui.log('Square delivery is uncertain: ' + String(error), { to: 'debug' });
|
|
126
|
+
}
|
|
127
|
+
}
|
|
128
|
+
}
|
|
129
|
+
if (current.closed) break;
|
|
130
|
+
const chunk = await current.stream.next();
|
|
131
|
+
if (chunk.done) break;
|
|
132
|
+
if (chunk.value.stream === 'stdout') current.buffer += chunk.value.text;
|
|
133
|
+
else $.ui.log(chunk.value.text, { to: 'debug' });
|
|
134
|
+
}
|
|
135
|
+
} catch (error) {
|
|
136
|
+
if (!current.closed) $.ui.log('Square Claude helper stopped: ' + String(error), { to: 'debug' });
|
|
137
|
+
} finally { await closeRuntime($, current); }
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
export function register(on) {
|
|
141
|
+
on('session.start', async ($, e, next) => {
|
|
142
|
+
await ensureRuntime($);
|
|
143
|
+
return next(e);
|
|
144
|
+
});
|
|
145
|
+
// /clear and /resume end the old session without another session.start.
|
|
146
|
+
on('prompt.submit', async ($, e, next) => {
|
|
147
|
+
await ensureRuntime($);
|
|
148
|
+
return next(e);
|
|
149
|
+
}).catch(($, e, next) => next(e));
|
|
150
|
+
on('turn.start', async ($, e, next) => {
|
|
151
|
+
const current = await ensureRuntime($);
|
|
152
|
+
await setMode($, current, 'busy', e.agentId);
|
|
153
|
+
return next(e);
|
|
154
|
+
});
|
|
155
|
+
on('turn.complete', async ($, e, next) => {
|
|
156
|
+
if (runtime && !runtime.closed) {
|
|
157
|
+
if (e.agentId !== undefined) {
|
|
158
|
+
if (e.isAborted) await setMode($, runtime, 'stopped', e.agentId);
|
|
159
|
+
await setMode($, runtime, 'ended', e.agentId);
|
|
160
|
+
} else await setMode($, runtime, e.isAborted ? 'stopped' : 'idle');
|
|
161
|
+
}
|
|
162
|
+
return next(e);
|
|
163
|
+
});
|
|
164
|
+
on('session.end', async ($, e, next) => {
|
|
165
|
+
const current = runtime;
|
|
166
|
+
if (current?.sessionId === e.sessionId) await closeRuntime($, current);
|
|
167
|
+
return next(e);
|
|
168
|
+
});
|
|
169
|
+
on('tool.call', { tool: /^mcp__plugin_square_square__/ }, async ($, e, next) => {
|
|
170
|
+
let current, call;
|
|
171
|
+
try {
|
|
172
|
+
current = await ensureRuntime($);
|
|
173
|
+
call = await rpc($, current, { op: 'mcp-begin', toolUseId: e.tool_use_id, cwd: await $.session.cwd(), agentId: e.agentId });
|
|
174
|
+
} catch { return { deny: 'Square MCP call identity is unavailable; this call did not run.' }; }
|
|
175
|
+
try { return await next(e); }
|
|
176
|
+
finally { await rpc($, current, { op: 'mcp-end', toolUseId: e.tool_use_id, token: call.token }).catch(() => undefined); }
|
|
177
|
+
}).catch(($, e, next) => next.called ? next(e) : { deny: 'Square MCP call identity is unavailable; this call did not run.' });
|
|
178
|
+
}
|
|
@@ -8,9 +8,9 @@ allowed-tools: Bash(square *)
|
|
|
8
8
|
|
|
9
9
|
Bring a thought, a question, an objection, or a gesture. Leave private tool chatter out; quiet is welcome too.
|
|
10
10
|
|
|
11
|
-
This skill explains what Square is and how to be present in it. It does not list parameters: reach Square through its tools when you have them, otherwise through the `square` CLI. The tool descriptions and `square <operation> --help` are the exact usage.
|
|
11
|
+
This skill explains what Square is and how to be present in it. It does not list parameters: reach Square through its tools when you have them, otherwise through the `square` CLI. The tool descriptions and `square <operation> --help` are the exact usage.
|
|
12
12
|
|
|
13
|
-
A square is a physical place where participants take in what happens and express thoughts or actions. Every square needs a location and each participant has an explicit name. Tools take the execution body from their live harness context. Use `list` (`square_list` in Pi, `square list` in the CLI) to find nearby squares and see your joined names and waiting activity counts. Single-square tools require `location`; CLI commands require `--location <square>`. Join with an explicit `--as <name>` (tool argument `as`).
|
|
13
|
+
A square is a physical place where participants take in what happens and express thoughts or actions. Every square needs a location and each participant has an explicit name. Tools take the execution body from their live harness context. Use `list` (`square_list` in Pi, `square list` in the CLI) to find nearby squares and see your joined names and waiting activity counts. Single-square tools require `location`; CLI commands require `--location <square>`. Join with an explicit `--as <name>` (tool argument `as`). Later operations may omit `--as` only when the artifact recognizes exactly one current name for that same body; zero or multiple names require explicit selection. A name never grants custody.
|
|
14
14
|
|
|
15
15
|
## The rhythm
|
|
16
16
|
|
|
@@ -6,7 +6,7 @@ select text, or copy it in full. The card follows the active Paseo theme on
|
|
|
6
6
|
desktop and mobile.
|
|
7
7
|
|
|
8
8
|
Requires Paseo 0.11.2 or newer with custom-message tool rows (#6090). Structured messages
|
|
9
|
-
carry producer-neutral origin, author, reference,
|
|
9
|
+
carry producer-neutral origin, author, reference, landing time, reach and full body details. Plain messages and
|
|
10
10
|
joining context use a neutral notification heading.
|
|
11
11
|
|
|
12
12
|
The plugin changes presentation only. It does not send prompts or alter model
|
package/plugins/paseo/README.md
CHANGED
|
@@ -17,7 +17,8 @@ are trusted, unsandboxed code running inside the Paseo app.
|
|
|
17
17
|
|
|
18
18
|
Requires Paseo >=0.11.2 with custom-message tool rows (#6090). Structured notification
|
|
19
19
|
cards use `details.items` with producer-neutral presentation facts: `kind: 'presentation'`, `origin`,
|
|
20
|
-
|
|
20
|
+
`at` (landing time in epoch milliseconds), `reach` (`mention`, `reply`, `attention`, or `bell`),
|
|
21
|
+
optional `author` and `ref`, and the full Markdown `body`. Missing or malformed details fall
|
|
21
22
|
back to the original plain text.
|
|
22
23
|
|
|
23
24
|
## Display
|
|
@@ -26,28 +27,24 @@ Only synthetic `tool_call` items with `metadata.customType === "square"` and
|
|
|
26
27
|
plain-text detail are projected into a card. Real tools, other custom messages,
|
|
27
28
|
and unrecognized rows remain unchanged.
|
|
28
29
|
|
|
29
|
-
The
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
input
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
the
|
|
47
|
-
|
|
48
|
-
An accent glyph, author and origin label identify the card. The header and its arrow
|
|
49
|
-
toggle an inline drawer. It uses Paseo theme colors, selectable text, accessible
|
|
50
|
-
expansion state and compact layout on narrow displays. Removing the plugin restores
|
|
30
|
+
The header and messages use `metadata.details.items` directly, without parsing
|
|
31
|
+
notification envelopes for attribution. Messages remain oldest first in both states.
|
|
32
|
+
Both states keep the same Markdown, inline authors, reply and bell labels, timestamps and individual guides; a mention is marked by its guide colour alone. Contiguous
|
|
33
|
+
activity references from one origin form a range; gaps stay explicit. Body text
|
|
34
|
+
containing Square tags is ordinary content.
|
|
35
|
+
|
|
36
|
+
Both states use one 32,000 UTF-16-unit source display budget, including omission lines.
|
|
37
|
+
Long bodies retain their beginning and end without splitting surrogate pairs.
|
|
38
|
+
Collapsed cards clip the Markdown at four 20px lines; the edge may cut through a line.
|
|
39
|
+
Expansion raises the limit to 260px and enables internal scrolling without replacing the content.
|
|
40
|
+
Click anywhere on the card to expand or collapse it, including short cards. A header chevron appears only
|
|
41
|
+
when the reported content height exceeds the collapsed limit.
|
|
42
|
+
No custom line measurement is needed. Plain or malformed details use bounded `detail.text`
|
|
43
|
+
with a neutral heading. Rendering never fetches producer storage or marks activity seen.
|
|
44
|
+
Live events and fetched history use the same transformer, and model input is unchanged.
|
|
45
|
+
|
|
46
|
+
The slate palette follows the host theme. The whole card toggles the height limit and
|
|
47
|
+
exposes its expansion state to assistive technology. Removing the plugin restores
|
|
51
48
|
Paseo's ordinary custom-message row.
|
|
52
49
|
|
|
53
50
|
## Develop
|
|
@@ -1,30 +1,41 @@
|
|
|
1
1
|
import { useMemo, type ReactNode } from "react";
|
|
2
|
-
import { Platform, Text, View, type TextStyle } from "react-native";
|
|
3
|
-
import type
|
|
2
|
+
import { Platform, ScrollView, Text, View, type TextStyle } from "react-native";
|
|
3
|
+
import { withAlpha, type SquarePalette } from "./palette";
|
|
4
4
|
import { Lexer, lexer, type Token, type Tokens } from "marked";
|
|
5
5
|
|
|
6
6
|
export const MONO = Platform.select({ ios: "Menlo", android: "monospace", default: "ui-monospace, Menlo, monospace" });
|
|
7
7
|
|
|
8
|
-
function makeStyles(
|
|
8
|
+
function makeStyles(palette: SquarePalette) {
|
|
9
|
+
const colors = palette.theme.colors;
|
|
10
|
+
const hairline = { borderColor: colors.border, borderWidth: 0.5 };
|
|
9
11
|
return {
|
|
10
|
-
block: { gap:
|
|
11
|
-
text: { color:
|
|
12
|
-
|
|
12
|
+
block: { gap: 8 },
|
|
13
|
+
text: { color: palette.body, fontSize: 13.5, lineHeight: 20 },
|
|
14
|
+
// Size carries only the top two levels; deeper headings are weight alone, so a card never shouts.
|
|
15
|
+
heading: { fontWeight: "600" as const, color: colors.foreground, letterSpacing: -0.1 },
|
|
16
|
+
headingSizes: [{ fontSize: 15.5, lineHeight: 22 }, { fontSize: 14.5, lineHeight: 21 }] as TextStyle[],
|
|
17
|
+
lead: { color: palette.name, fontWeight: "600" as const },
|
|
13
18
|
strong: { fontWeight: "600" as const, color: colors.foreground },
|
|
14
|
-
em: {
|
|
19
|
+
em: { fontStyle: "italic" as const },
|
|
15
20
|
del: { color: colors.foregroundMuted, textDecorationLine: "line-through" as const },
|
|
16
|
-
link: { color: colors.accent, textDecorationLine: "underline" as const },
|
|
17
|
-
inlineCode: { fontFamily: MONO },
|
|
18
|
-
codeBlock: { backgroundColor: colors.surface0, borderRadius: 4
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
21
|
+
link: { color: colors.accent, textDecorationLine: "underline" as const, textDecorationColor: withAlpha(colors.accent, 0.4) },
|
|
22
|
+
inlineCode: { fontFamily: MONO, fontSize: 12, color: colors.foreground, backgroundColor: palette.inlineCode, borderRadius: 3 },
|
|
23
|
+
codeBlock: { ...hairline, backgroundColor: colors.surface0, borderRadius: 4 },
|
|
24
|
+
codeContent: { paddingHorizontal: 10, paddingVertical: 8 },
|
|
25
|
+
codeText: { fontFamily: MONO, fontSize: 12, lineHeight: 18, color: colors.foreground },
|
|
26
|
+
quote: { borderLeftWidth: 2, borderLeftColor: colors.border, paddingLeft: 10, gap: 8 },
|
|
27
|
+
quoteText: { color: colors.foregroundMuted },
|
|
28
|
+
separator: { height: 0.5, backgroundColor: colors.border, marginVertical: 4 },
|
|
29
|
+
list: { gap: 4 },
|
|
30
|
+
listRow: { flexDirection: "row" as const },
|
|
31
|
+
// A fixed gutter gives wrapped lines a hanging indent; ordinals align on their right edge.
|
|
32
|
+
bullet: { color: colors.foregroundMuted, fontSize: 13.5, lineHeight: 20, minWidth: 18, paddingRight: 6,
|
|
33
|
+
textAlign: "right" as const, fontVariant: ["tabular-nums" as const] },
|
|
34
|
+
listItem: { flex: 1, gap: 4 },
|
|
35
|
+
table: { ...hairline, borderRadius: 4, overflow: "hidden" as const },
|
|
36
|
+
tableHead: { backgroundColor: colors.surface0 },
|
|
37
|
+
tableRow: { flexDirection: "row" as const, borderTopWidth: 0.5, borderTopColor: colors.border },
|
|
38
|
+
tableCell: { paddingVertical: 4, paddingHorizontal: 8 },
|
|
28
39
|
};
|
|
29
40
|
}
|
|
30
41
|
|
|
@@ -33,27 +44,27 @@ type Styles = ReturnType<typeof makeStyles>;
|
|
|
33
44
|
/** Children of a token, if it has any: `Token` is a union, so the optional field needs one cast. */
|
|
34
45
|
const children = (node: Token | { tokens?: Token[] }): Token[] => (node as { tokens?: Token[] }).tokens ?? [];
|
|
35
46
|
|
|
36
|
-
const
|
|
37
|
-
|
|
38
|
-
entity === "lt" ? "<" : entity === "gt" ? ">" : entity === "quot" ? '"' : entity === "#39" ? "'" : "&");
|
|
47
|
+
const ENTITIES: Record<string, string> = { lt: "<", gt: ">", quot: '"', "#39": "'", amp: "&" };
|
|
48
|
+
const unescape = (text: string): string => text.replace(/&(lt|gt|quot|#39|amp);/g, (_, entity: string) => ENTITIES[entity]!);
|
|
39
49
|
|
|
40
|
-
|
|
50
|
+
/** Inline runs nest inside their block's Text, so they carry only what differs from it. */
|
|
51
|
+
function renderInlines(tokens: Token[], styles: Styles, prefix: string): ReactNode[] {
|
|
41
52
|
return tokens.map((token, index) => {
|
|
42
53
|
const key = `${prefix}-${index}`;
|
|
43
|
-
const nested = (style
|
|
44
|
-
<Text key={key} style={
|
|
54
|
+
const nested = (style?: TextStyle): ReactNode => (
|
|
55
|
+
<Text key={key} style={style}>{renderInlines(children(token), styles, key)}</Text>
|
|
45
56
|
);
|
|
46
57
|
switch (token.type) {
|
|
47
58
|
case "strong": return nested(styles.strong);
|
|
48
59
|
case "em": return nested(styles.em);
|
|
49
60
|
case "del": return nested(styles.del);
|
|
50
61
|
case "link": return nested(styles.link);
|
|
51
|
-
case "codespan": return <Text key={key} style={
|
|
62
|
+
case "codespan": return <Text key={key} style={styles.inlineCode}>{` ${unescape(token.text)} `}</Text>;
|
|
52
63
|
case "br": return "\n";
|
|
53
64
|
case "image": return token.text;
|
|
54
65
|
default: {
|
|
55
66
|
const inline = token as Tokens.Generic;
|
|
56
|
-
if (inline.tokens !== undefined) return nested(
|
|
67
|
+
if (inline.tokens !== undefined) return nested();
|
|
57
68
|
return unescape(inline.text ?? inline.raw);
|
|
58
69
|
}
|
|
59
70
|
}
|
|
@@ -74,13 +85,13 @@ function renderBlocks(tokens: Token[], styles: Styles, prefix: string, textStyle
|
|
|
74
85
|
return <View key={key} style={styles.separator} />;
|
|
75
86
|
case "code":
|
|
76
87
|
return (
|
|
77
|
-
<
|
|
88
|
+
<ScrollView key={key} horizontal style={styles.codeBlock} contentContainerStyle={styles.codeContent}>
|
|
78
89
|
<Text selectable style={styles.codeText}>{token.text}</Text>
|
|
79
|
-
</
|
|
90
|
+
</ScrollView>
|
|
80
91
|
);
|
|
81
92
|
case "heading":
|
|
82
93
|
return (
|
|
83
|
-
<Text key={key} style={[styles.text, textStyle, styles.
|
|
94
|
+
<Text key={key} style={[styles.text, textStyle, styles.heading, styles.headingSizes[(token as Tokens.Heading).depth - 1]]}>
|
|
84
95
|
{renderInlines(children(token), styles, key)}
|
|
85
96
|
</Text>
|
|
86
97
|
);
|
|
@@ -90,11 +101,11 @@ function renderBlocks(tokens: Token[], styles: Styles, prefix: string, textStyle
|
|
|
90
101
|
const list = token as Tokens.List;
|
|
91
102
|
const start = typeof list.start === "number" ? list.start : 1;
|
|
92
103
|
return (
|
|
93
|
-
<View key={key} style={
|
|
104
|
+
<View key={key} style={styles.list}>
|
|
94
105
|
{list.items.map((item, itemIndex) => (
|
|
95
106
|
<View key={`${key}-${itemIndex}`} style={styles.listRow}>
|
|
96
107
|
<Text style={styles.bullet}>
|
|
97
|
-
{item.task ? (item.checked === true ? "☑" : "☐") : list.ordered ? `${start + itemIndex}.` : "
|
|
108
|
+
{item.task ? (item.checked === true ? "☑" : "☐") : list.ordered ? `${start + itemIndex}.` : "•"}
|
|
98
109
|
</Text>
|
|
99
110
|
<View style={styles.listItem}>{renderBlocks(children(item), styles, `${key}-${itemIndex}`, textStyle)}</View>
|
|
100
111
|
</View>
|
|
@@ -105,19 +116,26 @@ function renderBlocks(tokens: Token[], styles: Styles, prefix: string, textStyle
|
|
|
105
116
|
case "table": {
|
|
106
117
|
const table = token as Tokens.Table;
|
|
107
118
|
const rows = [table.header, ...table.rows];
|
|
119
|
+
// Columns size to their widest cell (within reason) and the table scrolls sideways
|
|
120
|
+
// on narrow screens instead of crushing every cell to a sliver.
|
|
121
|
+
const widths = table.header.map((_, column) => Math.min(240, Math.max(56,
|
|
122
|
+
...rows.map(cells => (cells[column]?.text.length ?? 0) * 7.5 + 16))));
|
|
108
123
|
return (
|
|
109
|
-
<
|
|
110
|
-
{
|
|
111
|
-
|
|
112
|
-
{
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
124
|
+
<ScrollView key={key} horizontal>
|
|
125
|
+
<View style={styles.table}>
|
|
126
|
+
{rows.map((cells, rowIndex) => (
|
|
127
|
+
<View key={`${key}-${rowIndex}`} style={rowIndex === 0 ? [styles.tableRow, styles.tableHead, { borderTopWidth: 0 }] : styles.tableRow}>
|
|
128
|
+
{cells.map((cell, cellIndex) => (
|
|
129
|
+
<Text
|
|
130
|
+
key={`${key}-${rowIndex}-${cellIndex}`}
|
|
131
|
+
style={[styles.text, textStyle, styles.tableCell, { width: widths[cellIndex], textAlign: table.align[cellIndex] ?? "left" },
|
|
132
|
+
rowIndex === 0 ? styles.strong : undefined]}
|
|
133
|
+
>{renderInlines(children(cell), styles, `${key}-${rowIndex}-${cellIndex}`)}</Text>
|
|
134
|
+
))}
|
|
135
|
+
</View>
|
|
136
|
+
))}
|
|
137
|
+
</View>
|
|
138
|
+
</ScrollView>
|
|
121
139
|
);
|
|
122
140
|
}
|
|
123
141
|
default: {
|
|
@@ -132,58 +150,33 @@ function renderBlocks(tokens: Token[], styles: Styles, prefix: string, textStyle
|
|
|
132
150
|
});
|
|
133
151
|
}
|
|
134
152
|
|
|
135
|
-
/**
|
|
136
|
-
function
|
|
137
|
-
return <Text style={styles.lead}>{name}{"\u2002"}</Text>;
|
|
138
|
-
}
|
|
139
|
-
|
|
140
|
-
/** One line (or a few) of Markdown, inline only: the collapsed preview on a notification card. */
|
|
141
|
-
export function SquareInline({ text, theme, numberOfLines, lead }: {
|
|
153
|
+
/** Attribution stays inline; a separate timestamp column reserves space on the first render. */
|
|
154
|
+
export function SquareMarkdownBody({ text, palette, author, reach, time }: {
|
|
142
155
|
text: string;
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
156
|
+
palette: SquarePalette;
|
|
157
|
+
author?: string;
|
|
158
|
+
reach?: string;
|
|
159
|
+
time?: string;
|
|
146
160
|
}) {
|
|
147
|
-
const styles = useMemo(() => makeStyles(
|
|
148
|
-
return (
|
|
149
|
-
<Text numberOfLines={numberOfLines} style={styles.text}>
|
|
150
|
-
{lead !== undefined && <Lead name={lead} styles={styles} />}
|
|
151
|
-
{renderInlines(Lexer.lexInline(text), styles, "inline")}
|
|
152
|
-
</Text>
|
|
153
|
-
);
|
|
154
|
-
}
|
|
155
|
-
|
|
156
|
-
/**
|
|
157
|
-
* A body of Markdown: no envelope — the words only, at full width.
|
|
158
|
-
* `lead` runs the author into the first line.
|
|
159
|
-
*/
|
|
160
|
-
export function SquareMarkdownBody({ text, theme, lead }: {
|
|
161
|
-
text: string;
|
|
162
|
-
theme: PluginTheme;
|
|
163
|
-
lead?: string;
|
|
164
|
-
}) {
|
|
165
|
-
const styles = useMemo(() => makeStyles(theme.colors), [theme.colors]);
|
|
161
|
+
const styles = useMemo(() => makeStyles(palette), [palette]);
|
|
166
162
|
const tokens = useMemo(() => lexer(text).filter(token => token.type !== "space"), [text]);
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
{pending !== undefined && <Text style={styles.text}><Lead name={pending} styles={styles} /></Text>}
|
|
187
|
-
</View>
|
|
188
|
-
);
|
|
163
|
+
const attribution = <>
|
|
164
|
+
{author !== undefined && <Text style={styles.lead}>{author}</Text>}
|
|
165
|
+
{reach !== undefined && <Text style={{ fontSize: 11, fontWeight: "500", color: palette.warm }}>{" "}{reach}</Text>}
|
|
166
|
+
{(author !== undefined || reach !== undefined) && "\u2002"}
|
|
167
|
+
</>;
|
|
168
|
+
const timeStyle: TextStyle = { color: palette.faint, fontSize: 11, lineHeight: 20, fontVariant: ["tabular-nums"] };
|
|
169
|
+
const first = tokens[0];
|
|
170
|
+
const runIn = first?.type === "paragraph" || first?.type === "heading";
|
|
171
|
+
const hasHeader = author !== undefined || reach !== undefined || time !== undefined;
|
|
172
|
+
const firstLine = <>{attribution}{runIn && renderInlines(children(first), styles, "first")}</>;
|
|
173
|
+
return <View style={styles.block}>
|
|
174
|
+
{hasHeader && <View style={{ flexDirection: "row", alignItems: "flex-start", gap: 12 }}>
|
|
175
|
+
<Text selectable={Platform.OS === "web"} style={[styles.text, { flex: 1, minWidth: 0 }]}>
|
|
176
|
+
{firstLine}
|
|
177
|
+
</Text>
|
|
178
|
+
{time !== undefined && <Text style={[timeStyle, { flexShrink: 0 }]}>{time}</Text>}
|
|
179
|
+
</View>}
|
|
180
|
+
{renderBlocks(hasHeader && runIn ? tokens.slice(1) : tokens, styles, "body")}
|
|
181
|
+
</View>;
|
|
189
182
|
}
|
|
@@ -7,22 +7,22 @@ import type { PluginTheme } from "@getpaseo/plugin";
|
|
|
7
7
|
* the card earns its identity from colour and the seal, not decoration.
|
|
8
8
|
*/
|
|
9
9
|
const DARK = {
|
|
10
|
-
surface0: "#
|
|
10
|
+
surface0: "#1a1d21", // inset / code
|
|
11
11
|
surface1: "#252a2e", // card (matte slate paper)
|
|
12
|
-
surface2: "#
|
|
13
|
-
border: "#
|
|
14
|
-
foreground: "#
|
|
12
|
+
surface2: "#1a1d21", // inset / code
|
|
13
|
+
border: "#363d44",
|
|
14
|
+
foreground: "#e4e6ea",
|
|
15
15
|
foregroundMuted: "#858b94",
|
|
16
16
|
accent: "#94a6b5", // plaza ■ / links
|
|
17
17
|
} as const;
|
|
18
18
|
|
|
19
19
|
const LIGHT = {
|
|
20
|
-
surface0: "#
|
|
21
|
-
surface1: "#
|
|
22
|
-
surface2: "#
|
|
23
|
-
border: "#
|
|
24
|
-
foreground: "#
|
|
25
|
-
foregroundMuted: "#
|
|
20
|
+
surface0: "#e9edf0",
|
|
21
|
+
surface1: "#f6f7f9", // card (matte slate paper)
|
|
22
|
+
surface2: "#e9edf0",
|
|
23
|
+
border: "#dde2e7",
|
|
24
|
+
foreground: "#1f2226",
|
|
25
|
+
foregroundMuted: "#6b727b",
|
|
26
26
|
accent: "#71869a",
|
|
27
27
|
} as const;
|
|
28
28
|
|
|
@@ -33,7 +33,14 @@ export interface SquarePalette {
|
|
|
33
33
|
card: { borderRadius: number; borderWidth: number; boxShadow: string };
|
|
34
34
|
/** The square ■ seal, unrounded and flat. */
|
|
35
35
|
seal: { borderRadius: number };
|
|
36
|
-
/**
|
|
36
|
+
/** Body and metadata tones derived from the slate theme. */
|
|
37
|
+
body: string;
|
|
38
|
+
faint: string;
|
|
39
|
+
name: string;
|
|
40
|
+
warm: string;
|
|
41
|
+
directGuide: string;
|
|
42
|
+
inlineCode: string;
|
|
43
|
+
/** The thin message guide under the header. */
|
|
37
44
|
quote: { borderLeftWidth: number; borderLeftColor: string; paddingLeft: number };
|
|
38
45
|
}
|
|
39
46
|
|
|
@@ -45,6 +52,18 @@ function brightness(hex: string): number {
|
|
|
45
52
|
+ 0.114 * parseInt(rgb.slice(4, 6), 16);
|
|
46
53
|
}
|
|
47
54
|
|
|
55
|
+
export function withAlpha(hex: string, opacity: number): string {
|
|
56
|
+
const rgb = hex.length === 4 ? "#" + hex.slice(1).replace(/./g, digit => digit + digit) : hex.slice(0, 7);
|
|
57
|
+
return `${rgb}${Math.round(opacity * 255).toString(16).padStart(2, "0")}`;
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
function mix(first: string, second: string, amount: number): string {
|
|
61
|
+
const rgb = (hex: string) => (hex.length === 4 ? hex.slice(1).replace(/./g, digit => digit + digit) : hex.slice(1, 7));
|
|
62
|
+
const a = rgb(first), b = rgb(second);
|
|
63
|
+
return "#" + [0, 2, 4].map(offset => Math.round(parseInt(a.slice(offset, offset + 2), 16) * amount
|
|
64
|
+
+ parseInt(b.slice(offset, offset + 2), 16) * (1 - amount)).toString(16).padStart(2, "0")).join("");
|
|
65
|
+
}
|
|
66
|
+
|
|
48
67
|
export function squarePalette(theme: PluginTheme): SquarePalette {
|
|
49
68
|
// The SDK exposes colors, not appearance. Use the host theme, never the OS color scheme.
|
|
50
69
|
const dark = brightness(theme.colors.foreground) > brightness(theme.colors.surface0);
|
|
@@ -59,6 +78,12 @@ export function squarePalette(theme: PluginTheme): SquarePalette {
|
|
|
59
78
|
} },
|
|
60
79
|
card: { borderRadius: 4, borderWidth: 0.5, boxShadow: "none" },
|
|
61
80
|
seal: { borderRadius: 0 },
|
|
62
|
-
|
|
81
|
+
body: mix(palette.foreground, palette.surface1, 0.9),
|
|
82
|
+
faint: mix(palette.foregroundMuted, palette.surface1, 0.7),
|
|
83
|
+
name: mix(palette.accent, palette.foreground, 0.8),
|
|
84
|
+
warm: theme.colors.statusDanger,
|
|
85
|
+
directGuide: withAlpha(theme.colors.statusDanger, 0.9),
|
|
86
|
+
inlineCode: withAlpha(palette.accent, 0.1),
|
|
87
|
+
quote: { borderLeftWidth: 1, borderLeftColor: withAlpha(theme.colors.statusWarning, 0.45), paddingLeft: 10 },
|
|
63
88
|
};
|
|
64
89
|
}
|