@parley-im/parley 0.2.10 → 0.2.12
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/README.md +4 -4
- package/dist/access.js +1 -1
- package/dist/account.js +1 -1
- package/dist/cli.js +8 -8
- package/dist/daemon.js +1 -1
- package/dist/mcp.js +5 -5
- package/dist/relay.js +1 -1
- package/dist/server.js +3 -3
- package/package.json +1 -1
- package/web/app.css +85 -13
- package/web/chat.html +2 -2
- package/web/chat.js +5 -5
- package/web/dashboard.js +1 -1
- package/web/home.js +55 -11
- package/web/panel.html +1 -1
- package/web/panel.js +1 -1
package/README.md
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
#
|
|
1
|
+
# Parley
|
|
2
2
|
|
|
3
3
|
Send someone a link. They open a private chat with your own Claude or Codex, running read-only in your project folder, and tell it what they think. It asks follow-up questions, answers their questions by reading the code, and saves everything for you.
|
|
4
4
|
|
|
@@ -6,7 +6,7 @@ The agent runs on your computer, with your Claude or Codex login and plan. Links
|
|
|
6
6
|
|
|
7
7
|
## Using it
|
|
8
8
|
|
|
9
|
-
Link your computer to your
|
|
9
|
+
Link your computer to your Parley account once:
|
|
10
10
|
|
|
11
11
|
```bash
|
|
12
12
|
parley login
|
|
@@ -68,9 +68,9 @@ Each session is a fresh agent conversation in the project folder, guided by a ch
|
|
|
68
68
|
It can read the project folder and nothing else on your computer. Inside it, a blocked list keeps it from secrets (`.env` files, keys and certificates, `.ssh`, cloud and package-manager logins, `secrets/`), from other guests' conversations in any `.parley` folder, and always from parley's own database. Each agent's own sandbox enforces that, not just the charter (which asks too). `src/access.ts` builds the rules; sessions won't start in your home folder. Both are settings (see Config).
|
|
69
69
|
|
|
70
70
|
- **Claude:** `claude -p --restricted --tools Read,Grep,Glob --strict-mcp-config`, plus `Read(...)` deny rules from the blocked list. No command, write or web tools; `--restricted` confines the file tools to the project; your MCP servers are left out.
|
|
71
|
-
- **Codex:** `codex exec --ignore-user-config` under a permission profile that can read the project and the system files tools need (`:minimal`), with the blocked list denied, no writes and no network. Apps, browser, computer use, plugins, hooks and web search are off; your model and reasoning effort carry over. Codex re-runs itself inside that sandbox, so
|
|
71
|
+
- **Codex:** `codex exec --ignore-user-config` under a permission profile that can read the project and the system files tools need (`:minimal`), with the blocked list denied, no writes and no network. Apps, browser, computer use, plugins, hooks and web search are off; your model and reasoning effort carry over. Codex re-runs itself inside that sandbox, so Parley starts it by its real path and lets it read that one file.
|
|
72
72
|
|
|
73
|
-
Before a session is made,
|
|
73
|
+
Before a session is made, Parley checks the agent is installed and new enough for these flags, and says what to update if not.
|
|
74
74
|
|
|
75
75
|
Each reply records which model answered, on which login (`auth: "none"` means your subscription, not an API key), and what it would cost at API prices.
|
|
76
76
|
|
package/dist/access.js
CHANGED
|
@@ -44,7 +44,7 @@ export function agentProblem(config, agent) {
|
|
|
44
44
|
try {
|
|
45
45
|
const help = execFileSync(bin, NEEDS[agent].args, { encoding: "utf8", timeout: 20_000, env: cleanEnv(), stdio: ["ignore", "pipe", "pipe"] });
|
|
46
46
|
if (NEEDS[agent].flags.some((flag) => !help.includes(flag))) {
|
|
47
|
-
problem = `This ${NAMES[agent]} is too old for
|
|
47
|
+
problem = `This ${NAMES[agent]} is too old for Parley's sandbox. Update it (${UPDATE[agent]}) and try again.`;
|
|
48
48
|
}
|
|
49
49
|
}
|
|
50
50
|
catch (error) {
|
package/dist/account.js
CHANGED
|
@@ -34,7 +34,7 @@ async function call(creds, path, init = {}) {
|
|
|
34
34
|
if (res.status === 401)
|
|
35
35
|
throw Object.assign(new Error("This computer isn't linked to your parley account anymore. Run parley login."), { unlinked: true });
|
|
36
36
|
if (!res.ok)
|
|
37
|
-
throw new Error((await res.json().catch(() => null))?.message ?? `
|
|
37
|
+
throw new Error((await res.json().catch(() => null))?.message ?? `Parley's servers answered ${res.status}`);
|
|
38
38
|
return res.json();
|
|
39
39
|
}
|
|
40
40
|
// What parley.im is told about a session: its id and a title for link
|
package/dist/cli.js
CHANGED
|
@@ -12,8 +12,8 @@ import { readState, linkFor, dashboardUrl, isServerUp } from "./state.js";
|
|
|
12
12
|
import { transcript, summaryLine } from "./transcript.js";
|
|
13
13
|
import { startSession, endSession, deleteSession, setReplyLimit } from "./control.js";
|
|
14
14
|
import { login, readCredentials, clearCredentials, relay as registry } from "./account.js";
|
|
15
|
-
const HELP = `
|
|
16
|
-
computer, and
|
|
15
|
+
const HELP = `Parley: someone chats with a read-only Claude or Codex running in this project, on this
|
|
16
|
+
computer, and what they share comes back to this project. Good for feedback from guests, or any interview.
|
|
17
17
|
|
|
18
18
|
If you're an agent and the user asks for "a parley" (or to get someone's take on this project):
|
|
19
19
|
|
|
@@ -37,16 +37,16 @@ Commands
|
|
|
37
37
|
parley limit ID N Let a session give N replies in total
|
|
38
38
|
parley end ID Kill its link for good; the transcript stays
|
|
39
39
|
parley delete ID Erase it, screenshots included
|
|
40
|
-
parley login | logout | whoami Link this computer to your
|
|
41
|
-
parley start | stop | status The background server (holds the connection to
|
|
40
|
+
parley login | logout | whoami Link this computer to your Parley account
|
|
41
|
+
parley start | stop | status The background server (holds the connection to Parley)
|
|
42
42
|
parley panel Open the control panel (localhost)
|
|
43
|
-
parley install Add
|
|
43
|
+
parley install Add Parley's MCP tools to Claude Code and Codex; start at login
|
|
44
44
|
parley uninstall Undo install (your conversations stay)
|
|
45
45
|
parley serve | mcp Run the server in the foreground | MCP over stdio
|
|
46
46
|
parley --version
|
|
47
47
|
|
|
48
48
|
Conversations and screenshots live in each project's .parley folder (kept out of git), with a
|
|
49
|
-
readable transcript.md per session. Settings and your
|
|
49
|
+
readable transcript.md per session. Settings and your Parley login live in ${paths.home}.`;
|
|
50
50
|
const CLI_PATH = fileURLToPath(import.meta.url);
|
|
51
51
|
async function main() {
|
|
52
52
|
const [command, ...rest] = process.argv.slice(2);
|
|
@@ -253,14 +253,14 @@ async function install(config) {
|
|
|
253
253
|
if (config.claude.bin.startsWith("/")) {
|
|
254
254
|
run(config.claude.bin, ["mcp", "remove", "--scope", "user", "parley"]);
|
|
255
255
|
const ok = run(config.claude.bin, ["mcp", "add", "--scope", "user", "parley", "--", ...launch]);
|
|
256
|
-
console.log(ok ? "Claude Code: added the
|
|
256
|
+
console.log(ok ? "Claude Code: added the Parley MCP server (user scope)." : "Claude Code: couldn't add the MCP server.");
|
|
257
257
|
}
|
|
258
258
|
else
|
|
259
259
|
console.log("Claude Code: not found, skipped.");
|
|
260
260
|
if (config.codex.bin.startsWith("/")) {
|
|
261
261
|
run(config.codex.bin, ["mcp", "remove", "parley"]);
|
|
262
262
|
const ok = run(config.codex.bin, ["mcp", "add", "parley", "--", ...launch]);
|
|
263
|
-
console.log(ok ? "Codex: added the
|
|
263
|
+
console.log(ok ? "Codex: added the Parley MCP server." : "Codex: couldn't add the MCP server.");
|
|
264
264
|
}
|
|
265
265
|
else
|
|
266
266
|
console.log("Codex: not found, skipped.");
|
package/dist/daemon.js
CHANGED
|
@@ -15,7 +15,7 @@ export async function ensureServer(config, waitMs = 20_000) {
|
|
|
15
15
|
const running = await serverHealth(config);
|
|
16
16
|
if (running?.version && running.version !== VERSION) {
|
|
17
17
|
if (compareVersions(running.version, VERSION) > 0) {
|
|
18
|
-
throw new Error(`
|
|
18
|
+
throw new Error(`Parley was updated to ${running.version}, but this process is still ${VERSION}. Restart this Claude Code or Codex session (or the parley command) to use the new version.`);
|
|
19
19
|
}
|
|
20
20
|
await stopServer(config);
|
|
21
21
|
await waitUntilDown(config);
|
package/dist/mcp.js
CHANGED
|
@@ -20,7 +20,7 @@ export async function runMcp() {
|
|
|
20
20
|
const currentLink = (id) => linkFor(store.session(id), config);
|
|
21
21
|
server.registerTool("create_session", {
|
|
22
22
|
title: "Create a feedback session",
|
|
23
|
-
description: "Create a
|
|
23
|
+
description: "Create a Parley session: a private link the user can send to a guest. The guest chats with a fresh, read-only " +
|
|
24
24
|
"agent in the project folder that hears their feedback, answers questions by reading the code, and records everything. " +
|
|
25
25
|
"Starts the local server and tunnel if needed. Returns the link to give the user. Only for guests the user trusts, " +
|
|
26
26
|
"since the guest can ask about anything in the project folder.",
|
|
@@ -83,14 +83,14 @@ export async function runMcp() {
|
|
|
83
83
|
}
|
|
84
84
|
else {
|
|
85
85
|
lines.push("Everything after the # in the link is the session's encryption key: share the whole link, and only with the guest. " +
|
|
86
|
-
"Replies come from this computer, so it needs to be awake with
|
|
86
|
+
"Replies come from this computer, so it needs to be awake with Parley running.");
|
|
87
87
|
}
|
|
88
88
|
lines.push(`Tell the user to send it only to someone they trust: ${session.guest} can ask about anything in this project folder.`);
|
|
89
89
|
return reply(lines.join("\n"));
|
|
90
90
|
});
|
|
91
91
|
server.registerTool("list_sessions", {
|
|
92
92
|
title: "List feedback sessions",
|
|
93
|
-
description: "List
|
|
93
|
+
description: "List Parley sessions, newest activity first, with how many guest messages are new since you last read each one. " +
|
|
94
94
|
"Open sessions include their current guest link.",
|
|
95
95
|
inputSchema: {
|
|
96
96
|
status: z.enum(["open", "ended", "all"]).optional().describe("Defaults to open."),
|
|
@@ -111,7 +111,7 @@ export async function runMcp() {
|
|
|
111
111
|
});
|
|
112
112
|
server.registerTool("get_transcript", {
|
|
113
113
|
title: "Read a feedback session",
|
|
114
|
-
description: "Read a
|
|
114
|
+
description: "Read a Parley session as Markdown: the brief, every message, which model interviewed, and paths to screenshots the " +
|
|
115
115
|
"guest shared. Set only_new to read just what came in since you last read it. Reading marks it read.",
|
|
116
116
|
inputSchema: {
|
|
117
117
|
session_id: z.string(),
|
|
@@ -125,7 +125,7 @@ export async function runMcp() {
|
|
|
125
125
|
});
|
|
126
126
|
server.registerTool("end_session", {
|
|
127
127
|
title: "End a feedback session",
|
|
128
|
-
description: "End a
|
|
128
|
+
description: "End a Parley session for good when the user is done with it. The guest's link stops working entirely and an open " +
|
|
129
129
|
"page clears itself. The transcript stays readable here. This can't be undone; make a new session to talk again.",
|
|
130
130
|
inputSchema: { session_id: z.string() },
|
|
131
131
|
}, async (args) => {
|
package/dist/relay.js
CHANGED
|
@@ -108,7 +108,7 @@ export class RelayLink {
|
|
|
108
108
|
return;
|
|
109
109
|
}
|
|
110
110
|
if (event.code === 4000) {
|
|
111
|
-
this.status = { state: "replaced", relayUrl: creds.relayUrl, error: "Another
|
|
111
|
+
this.status = { state: "replaced", relayUrl: creds.relayUrl, error: "Another Parley server on this computer took over the connection." };
|
|
112
112
|
return;
|
|
113
113
|
}
|
|
114
114
|
this.status = { state: "offline", relayUrl: creds.relayUrl, email: creds.account.email, error: `Reconnecting to ${creds.relayUrl}` };
|
package/dist/server.js
CHANGED
|
@@ -156,7 +156,7 @@ export async function serve() {
|
|
|
156
156
|
if (rest === "/" && req.method === "GET") {
|
|
157
157
|
const from = ownerName(config);
|
|
158
158
|
const html = readFileSync(join(WEB, "chat.html"), "utf8")
|
|
159
|
-
.replaceAll("{{TITLE}}", escapeHtml(`${session.project_name} ·
|
|
159
|
+
.replaceAll("{{TITLE}}", escapeHtml(`${session.project_name} · Parley`))
|
|
160
160
|
.replaceAll("{{PREVIEW_TITLE}}", escapeHtml(`${from ? `${from} wants` : "Someone wants"} your feedback on ${session.project_name}`))
|
|
161
161
|
.replaceAll("{{PREVIEW_TEXT}}", escapeHtml(`A private chat about ${session.project_name}.`))
|
|
162
162
|
.replace("{{CONFIG}}", JSON.stringify({ base: scope.base, owner: scope.owner, maxText: MAX_TEXT }).replaceAll("<", "\\u003c"));
|
|
@@ -245,7 +245,7 @@ export async function serve() {
|
|
|
245
245
|
// Already running (started at login, and again by hand, say): leave it be
|
|
246
246
|
// and exit cleanly, which also tells a login service not to retry.
|
|
247
247
|
if (await isServerUp(config)) {
|
|
248
|
-
console.log(`
|
|
248
|
+
console.log(`Parley: a server is already running on ${config.dashboardPort}; leaving it be.`);
|
|
249
249
|
process.exit(0);
|
|
250
250
|
}
|
|
251
251
|
guest.listen(config.port, "127.0.0.1");
|
|
@@ -255,7 +255,7 @@ export async function serve() {
|
|
|
255
255
|
server.once("error", reject);
|
|
256
256
|
})));
|
|
257
257
|
writeState(state);
|
|
258
|
-
console.log(`
|
|
258
|
+
console.log(`Parley: control panel http://localhost:${config.dashboardPort}, local links on ${config.port}`);
|
|
259
259
|
link.start();
|
|
260
260
|
runner.resume();
|
|
261
261
|
// Says goodbye to parley.im before exiting, so guests see this computer go
|
package/package.json
CHANGED
package/web/app.css
CHANGED
|
@@ -456,14 +456,72 @@ body.panel { overflow: auto; }
|
|
|
456
456
|
/* A session's status on the dashboard (not the chat's reply in progress,
|
|
457
457
|
which is li.live). */
|
|
458
458
|
span.live { display: inline-flex; align-items: center; gap: 7px; font-size: 13px; color: var(--muted); white-space: nowrap; }
|
|
459
|
-
|
|
460
|
-
.docs
|
|
461
|
-
.docs
|
|
462
|
-
.docs
|
|
463
|
-
.docs
|
|
464
|
-
.docs
|
|
465
|
-
.docs
|
|
466
|
-
.docs
|
|
459
|
+
/* The docs: a contents column beside the article. */
|
|
460
|
+
.docs-page { max-width: 1120px; }
|
|
461
|
+
.docs-layout { display: grid; grid-template-columns: 210px minmax(0, 1fr); gap: 56px; padding: 32px 0 80px; }
|
|
462
|
+
.docs-nav { position: sticky; top: 16px; align-self: start; max-height: calc(100vh - 32px); overflow-y: auto; display: grid; gap: 1px; font-size: 14px; }
|
|
463
|
+
.docs-nav .label { margin: 18px 0 6px 10px; }
|
|
464
|
+
.docs-nav .label:first-child { margin-top: 4px; }
|
|
465
|
+
.docs-nav a { color: var(--muted); text-decoration: none; padding: 5px 10px; border-radius: 7px; }
|
|
466
|
+
.docs-nav a:hover { color: var(--text); background: var(--subtle); }
|
|
467
|
+
.docs { min-width: 0; max-width: 760px; font-size: 15.5px; line-height: 1.65; }
|
|
468
|
+
.docs-head { padding-bottom: 8px; }
|
|
469
|
+
.docs-head .label { margin: 0 0 8px; }
|
|
470
|
+
.docs h1 { font-size: 38px; line-height: 1.1; letter-spacing: -0.03em; margin: 0 0 14px; }
|
|
471
|
+
.docs .lede { font-size: 18px; line-height: 1.6; color: var(--muted); margin: 0; }
|
|
472
|
+
.docs h2 { font-size: 23px; letter-spacing: -0.02em; margin: 52px 0 14px; padding-top: 28px; border-top: 1px solid var(--line); scroll-margin-top: 12px; }
|
|
473
|
+
.docs h3 { font-size: 16px; letter-spacing: -0.01em; margin: 26px 0 8px; }
|
|
474
|
+
.docs p { margin: 0 0 14px; }
|
|
475
|
+
.docs ul { padding-left: 1.2em; margin: 0 0 16px; }
|
|
476
|
+
.docs li { margin: 7px 0; }
|
|
477
|
+
.docs li::marker { color: var(--faint); }
|
|
478
|
+
.docs a { color: var(--accent); }
|
|
479
|
+
.docs code { font-family: var(--mono); font-size: 0.85em; background: var(--subtle); border: 1px solid var(--line); padding: 0.08em 0.35em; border-radius: 5px; white-space: nowrap; }
|
|
480
|
+
.docs pre { background: var(--panel); border: 1px solid var(--line); border-radius: 10px; padding: 14px 16px; overflow-x: auto; font-size: 13.5px; line-height: 1.65; margin: 0 0 16px; }
|
|
481
|
+
.docs pre code { background: none; border: 0; padding: 0; font-size: inherit; white-space: pre; }
|
|
482
|
+
.docs blockquote { margin: 0 0 12px; padding: 12px 16px; background: var(--panel); border: 1px solid var(--line); border-radius: 10px; color: var(--text); }
|
|
483
|
+
.docs blockquote::before { content: "you"; display: block; font-family: var(--mono); font-size: 11px; letter-spacing: 0.04em; color: var(--faint); margin-bottom: 2px; }
|
|
484
|
+
.docs table { width: 100%; border-collapse: collapse; font-size: 14.5px; line-height: 1.5; margin: 0 0 18px; }
|
|
485
|
+
.docs th { text-align: left; font-weight: 600; font-size: 12px; letter-spacing: 0.04em; text-transform: uppercase; color: var(--faint); padding: 8px 12px; border-bottom: 1px solid var(--line-strong); }
|
|
486
|
+
.docs td { padding: 10px 12px; border-bottom: 1px solid var(--line); vertical-align: top; }
|
|
487
|
+
.docs td:first-child { white-space: nowrap; }
|
|
488
|
+
.docs .table-wrap { overflow-x: auto; margin: 0 0 18px; }
|
|
489
|
+
.docs .table-wrap table { margin: 0; }
|
|
490
|
+
.docs .callout { margin: 18px 0; padding: 14px 16px; background: var(--panel); border: 1px solid var(--line); border-left: 3px solid var(--accent); border-radius: 10px; }
|
|
491
|
+
.docs .callout.warn { border-left-color: var(--warn); }
|
|
492
|
+
.docs .flow { display: grid; grid-template-columns: repeat(3, minmax(0, 1fr)); gap: 28px; margin: 22px 0; position: relative; }
|
|
493
|
+
.docs .flow div { display: grid; gap: 4px; align-content: start; padding: 14px 16px; background: var(--panel); border: 1px solid var(--line); border-radius: 12px; position: relative; }
|
|
494
|
+
.docs .flow div + div::before { content: "⇄"; position: absolute; left: -22px; top: 50%; transform: translateY(-50%); color: var(--accent); font-size: 16px; }
|
|
495
|
+
.docs .flow strong { font-size: 15px; }
|
|
496
|
+
.docs .flow span { font-size: 13.5px; line-height: 1.5; color: var(--muted); }
|
|
497
|
+
.docs .howto { list-style: none; counter-reset: step; padding: 0; margin: 22px 0; display: grid; gap: 18px; }
|
|
498
|
+
.docs .howto li { counter-increment: step; position: relative; padding-left: 44px; margin: 0; }
|
|
499
|
+
.docs .howto li::before { content: counter(step); position: absolute; left: 0; top: 0; width: 28px; height: 28px; border-radius: 50%; display: grid; place-items: center; font-family: var(--mono); font-size: 13px; font-weight: 700; color: var(--accent-ink); background: var(--accent-fill); }
|
|
500
|
+
.docs .howto h3 { margin: 2px 0 8px; }
|
|
501
|
+
.docs .howto p, .docs .howto pre { margin: 0 0 8px; }
|
|
502
|
+
.docs details.more { margin: 0 0 16px; border: 1px solid var(--line); border-radius: 10px; background: var(--panel); padding: 10px 14px; }
|
|
503
|
+
.docs details.more summary { cursor: pointer; font-weight: 600; font-size: 14.5px; }
|
|
504
|
+
.docs details.more[open] summary { margin-bottom: 10px; }
|
|
505
|
+
.docs details.more pre { background: var(--bg); }
|
|
506
|
+
.docs .faq dt { font-weight: 600; margin-top: 18px; }
|
|
507
|
+
.docs .faq dd { margin: 4px 0 0; color: var(--muted); }
|
|
508
|
+
@media (max-width: 860px) {
|
|
509
|
+
.docs-layout { grid-template-columns: 1fr; gap: 24px; padding-top: 20px; }
|
|
510
|
+
.docs-nav { position: static; max-height: none; grid-template-columns: 1fr 1fr; padding-bottom: 18px; border-bottom: 1px solid var(--line); }
|
|
511
|
+
.docs-nav .label { display: none; }
|
|
512
|
+
.docs h1 { font-size: 30px; }
|
|
513
|
+
.docs .flow { grid-template-columns: 1fr; gap: 26px; }
|
|
514
|
+
.docs .flow div + div::before { left: 50%; top: -21px; transform: translateX(-50%) rotate(90deg); }
|
|
515
|
+
}
|
|
516
|
+
/* Admin: page views and where visitors came from. */
|
|
517
|
+
.table-wrap { overflow-x: auto; }
|
|
518
|
+
table.views { width: 100%; border-collapse: collapse; font-size: 14px; background: var(--panel); border: 1px solid var(--line); border-radius: 10px; overflow: hidden; }
|
|
519
|
+
table.views th { text-align: right; font-weight: 600; font-size: 12px; letter-spacing: 0.04em; text-transform: uppercase; color: var(--faint); padding: 8px 14px; border-bottom: 1px solid var(--line); }
|
|
520
|
+
table.views td { text-align: right; padding: 7px 14px; border-bottom: 1px solid var(--line); }
|
|
521
|
+
table.views th:first-child, table.views td:first-child { text-align: left; white-space: nowrap; }
|
|
522
|
+
table.views tfoot td { font-weight: 600; border-bottom: 0; }
|
|
523
|
+
.referrers { list-style: none; padding: 0; margin: 0 0 40px; max-width: 420px; }
|
|
524
|
+
.referrers li { display: flex; justify-content: space-between; padding: 7px 0; border-bottom: 1px solid var(--line); font-size: 14px; }
|
|
467
525
|
.trow.site-row { grid-template-columns: 1fr auto; }
|
|
468
526
|
/* Long details shorten instead of pushing the buttons off a phone screen. */
|
|
469
527
|
.trow.site-row .who-cell { min-width: 0; }
|
|
@@ -484,23 +542,37 @@ span.live { display: inline-flex; align-items: center; gap: 7px; font-size: 13px
|
|
|
484
542
|
.demo-tabs button { font-family: var(--mono); font-size: 12px; padding: 6px 10px; border: 1px solid transparent; border-radius: 8px; background: none; color: var(--muted); cursor: pointer; }
|
|
485
543
|
.demo-tabs button:hover { color: var(--text); }
|
|
486
544
|
.demo-tabs button.on { background: var(--panel); border-color: var(--line); color: var(--text); }
|
|
487
|
-
|
|
545
|
+
/* A fixed frame, like the real chat: the conversation scrolls inside it. */
|
|
546
|
+
.demo-chat { height: 500px; display: flex; flex-direction: column; background: var(--panel); border: 1px solid var(--line); border-radius: 18px; box-shadow: var(--shadow); overflow: hidden; }
|
|
488
547
|
.demo-top { display: flex; align-items: center; gap: 8px; padding: 10px 14px; border-bottom: 1px solid var(--line); font-size: 14px; }
|
|
489
548
|
.demo-top .mark { width: 20px; height: 20px; }
|
|
490
549
|
.demo-top .product { font-family: var(--mono); font-weight: 700; letter-spacing: -0.04em; }
|
|
491
550
|
.demo-top .slash { color: var(--faint); }
|
|
492
551
|
.demo-top .here { font-weight: 600; flex: 1; }
|
|
493
|
-
.demo-msgs { list-style: none; margin: 0; padding: 18px 18px
|
|
552
|
+
.demo-msgs { flex: 1; min-height: 0; overflow: hidden; list-style: none; margin: 0; padding: 18px 18px 14px; display: flex; flex-direction: column; gap: 14px; }
|
|
494
553
|
.demo .msg { margin: 0; animation: rise 0.25s ease-out; }
|
|
495
554
|
.demo .msg.agent .body { font-size: 15.5px; }
|
|
496
555
|
.demo .msg.agent::before { top: 5px; }
|
|
497
556
|
.demo .msg.guest .bubble { font-size: 14.5px; max-width: 86%; }
|
|
498
557
|
.demo .steps { margin-top: 2px; }
|
|
499
|
-
|
|
500
|
-
|
|
558
|
+
/* The demo's composer is the real one, played: text types in, the send
|
|
559
|
+
button wakes, and the message goes. */
|
|
560
|
+
.demo-dock { padding: 8px 12px 12px; border-top: 1px solid var(--line); background: var(--bg); }
|
|
561
|
+
.demo .composer { box-shadow: none; transition: border-color 0.15s, box-shadow 0.15s; }
|
|
562
|
+
.demo .composer.focus { border-color: var(--accent); box-shadow: 0 0 0 3px var(--accent-soft); }
|
|
563
|
+
.demo-input { flex: 1; min-width: 0; min-height: 40px; padding: 8px 4px; font-size: 15px; line-height: 1.55; color: var(--text); }
|
|
564
|
+
.demo-placeholder { color: var(--faint); }
|
|
565
|
+
.demo .composer.focus .demo-input::after { content: ""; display: inline-block; width: 1.5px; height: 1.1em; margin-left: 1px; vertical-align: -3px; background: var(--text); animation: blink 1.06s steps(1) infinite; }
|
|
566
|
+
.demo .icon-btn { cursor: default; }
|
|
567
|
+
.demo .icon-btn:hover { background: transparent; color: var(--muted); }
|
|
568
|
+
.demo .send, .demo .send:hover { background: var(--accent-fill); color: var(--accent-ink); transition: transform 0.12s, background 0.15s; }
|
|
569
|
+
.demo .send.off { background: var(--subtle); color: var(--faint); }
|
|
570
|
+
.demo .send.press { transform: scale(0.9); }
|
|
571
|
+
.demo-top .chip { margin-left: 4px; }
|
|
572
|
+
.demo-top .icon-btn { width: 32px; height: 32px; margin-right: -6px; }
|
|
501
573
|
@keyframes rise { from { opacity: 0; transform: translateY(4px); } }
|
|
502
574
|
@media (prefers-reduced-motion: reduce) { .demo .msg { animation: none; } }
|
|
503
|
-
@media (max-width: 560px) { .demo-
|
|
575
|
+
@media (max-width: 560px) { .demo-chat { height: 560px; } .demo-msgs { padding: 14px 12px 12px; } }
|
|
504
576
|
.how { list-style: none; counter-reset: step; padding: 0; margin: 40px 0 0; display: grid; gap: 16px; grid-template-columns: repeat(auto-fit, minmax(240px, 1fr)); }
|
|
505
577
|
.how li { counter-increment: step; border: 1px solid var(--line); border-radius: 16px; padding: 22px; background: var(--panel); display: grid; gap: 10px; align-content: start; }
|
|
506
578
|
.how li::before { content: "0" counter(step); font-family: var(--mono); font-weight: 700; color: var(--accent); font-size: 13px; }
|
package/web/chat.html
CHANGED
|
@@ -18,7 +18,7 @@
|
|
|
18
18
|
<header class="topbar">
|
|
19
19
|
<nav class="crumbs" id="crumbs">
|
|
20
20
|
<!-- What parley is, in a new tab so the conversation stays put. -->
|
|
21
|
-
<a class="brand" id="home-link" href="https://parley.im" target="_blank" rel="noopener" title="
|
|
21
|
+
<a class="brand" id="home-link" href="https://parley.im" target="_blank" rel="noopener" title="Parley: let people talk to your agent">
|
|
22
22
|
<span class="mark" aria-hidden="true"><svg viewBox="0 0 64 64" aria-hidden="true"><path class="r1" d="M32 7L39.2 11.2L32 15.3Z"/><path class="r2" d="M39.2 11.2L39.2 19.5L32 15.3Z"/><path class="r1" d="M39.2 11.2L46.4 15.3L39.2 19.5Z"/><path class="r2" d="M46.4 15.3L46.4 23.7L39.2 19.5Z"/><path class="r1" d="M46.4 15.3L53.7 19.5L46.4 23.7Z"/><path class="r2" d="M53.7 19.5L53.7 27.8L46.4 23.7Z"/><path class="r1" d="M46.4 23.7L53.7 27.8L46.4 32Z"/><path class="r2" d="M53.7 27.8L53.7 36.2L46.4 32Z"/><path class="r1" d="M46.4 32L53.7 36.2L46.4 40.3Z"/><path class="r2" d="M53.7 36.2L53.7 44.5L46.4 40.3Z"/><path class="r1" d="M46.4 40.3L53.7 44.5L46.4 48.7Z"/><path class="r2" d="M46.4 40.3L46.4 48.7L39.2 44.5Z"/><path class="r1" d="M39.2 44.5L46.4 48.7L39.2 52.8Z"/><path class="r2" d="M39.2 44.5L39.2 52.8L32 48.7Z"/><path class="r1" d="M32 48.7L39.2 52.8L32 57Z"/><path class="r2" d="M32 48.7L32 57L24.8 52.8Z"/><path class="r1" d="M24.8 44.5L32 48.7L24.8 52.8Z"/><path class="r2" d="M24.8 44.5L24.8 52.8L17.6 48.7Z"/><path class="r1" d="M17.6 40.3L24.8 44.5L17.6 48.7Z"/><path class="r2" d="M17.6 40.3L17.6 48.7L10.3 44.5Z"/><path class="r1" d="M10.3 36.2L17.6 40.3L10.3 44.5Z"/><path class="r2" d="M17.6 32L17.6 40.3L10.3 36.2Z"/><path class="r1" d="M10.3 27.8L17.6 32L10.3 36.2Z"/><path class="r2" d="M17.6 23.7L17.6 32L10.3 27.8Z"/><path class="r1" d="M10.3 19.5L17.6 23.7L10.3 27.8Z"/><path class="r2" d="M17.6 15.3L17.6 23.7L10.3 19.5Z"/><path class="r1" d="M17.6 15.3L24.8 19.5L17.6 23.7Z"/><path class="r2" d="M24.8 11.2L24.8 19.5L17.6 15.3Z"/><path class="r1" d="M24.8 11.2L32 15.3L24.8 19.5Z"/><path class="r2" d="M32 7L32 15.3L24.8 11.2Z"/><path class="c1" d="M32 23.7L39.2 27.8L32 32Z"/><path class="c2" d="M39.2 27.8L39.2 36.2L32 32Z"/><path class="c1" d="M32 32L39.2 36.2L32 40.3Z"/><path class="c2" d="M32 32L32 40.3L24.8 36.2Z"/><path class="c1" d="M24.8 27.8L32 32L24.8 36.2Z"/><path class="c2" d="M32 23.7L32 32L24.8 27.8Z"/></svg></span><span class="product">parley</span>
|
|
23
23
|
</a><span class="slash">/</span><span class="here" id="project"> </span>
|
|
24
24
|
</nav>
|
|
@@ -48,7 +48,7 @@
|
|
|
48
48
|
<div class="attachments" id="attachments" hidden></div>
|
|
49
49
|
<div class="row">
|
|
50
50
|
<span class="prompt" aria-hidden="true">›</span>
|
|
51
|
-
<textarea id="input" rows="1" placeholder="
|
|
51
|
+
<textarea id="input" rows="1" placeholder="Write a message" autocomplete="off" autocorrect="on" spellcheck="true" aria-label="Message"></textarea>
|
|
52
52
|
<label class="icon-btn" title="Add a screenshot">
|
|
53
53
|
<input type="file" id="file" accept="image/png,image/jpeg,image/gif,image/webp" multiple hidden>
|
|
54
54
|
<svg viewBox="0 0 24 24" aria-hidden="true"><rect x="3.5" y="5" width="17" height="14" rx="2.5"/><path d="m4 16 4.5-4.5a1.5 1.5 0 0 1 2.1 0L15 16"/><path d="m13.5 14.5 1.7-1.7a1.5 1.5 0 0 1 2.1 0l2.7 2.7"/><circle cx="15.5" cy="9.5" r="1.2"/></svg>
|
package/web/chat.js
CHANGED
|
@@ -303,7 +303,7 @@
|
|
|
303
303
|
if (seen.size === before) break;
|
|
304
304
|
}
|
|
305
305
|
const projectName = session?.project ?? knownProject ?? "a project";
|
|
306
|
-
const lines = [`parley about ${projectName}, ${new Date().toLocaleDateString()}`, ""];
|
|
306
|
+
const lines = [`A parley about ${projectName}, ${new Date().toLocaleDateString()}`, ""];
|
|
307
307
|
for (const m of [...kept.values()].sort((a, b) => a.id - b.id)) {
|
|
308
308
|
if (m.role === "error") continue;
|
|
309
309
|
lines.push(`${m.role === "guest" ? "You" : agentName()} · ${new Date(m.createdAt).toLocaleString()}`, m.body.trim(), "");
|
|
@@ -354,7 +354,7 @@
|
|
|
354
354
|
end.onclick = () => endConversation(end);
|
|
355
355
|
const open = !ended && session?.status !== "ended";
|
|
356
356
|
els.sheetBody.replaceChildren(
|
|
357
|
-
el("p", {}, strong(who), " set this up to hear what you think of ", strong(projectName), sender ? `.
|
|
357
|
+
el("p", {}, strong(who), " set this up to hear what you think of ", strong(projectName), sender ? `. Parley checked it's really them, with Google.` : "."),
|
|
358
358
|
kv(facts),
|
|
359
359
|
el("div", { className: "sheet-actions" }, save, open ? end : null),
|
|
360
360
|
open ? el("p", { className: "fine" }, `Ending it stops this link working, for you and anyone else who has it. ${ownerName || "They"} keep${ownerName ? "s" : ""} everything said so far.`) : null,
|
|
@@ -367,7 +367,7 @@
|
|
|
367
367
|
? el(
|
|
368
368
|
"p",
|
|
369
369
|
{},
|
|
370
|
-
`Messages and screenshots are end-to-end encrypted between this page and ${theirs} computer.
|
|
370
|
+
`Messages and screenshots are end-to-end encrypted between this page and ${theirs} computer. Parley passes them along but can't read them; it knows who sent this link, the project's name and when messages go by, and counts how many times pages are opened each day. The key is the part of your link after the #, so anyone with the whole link can read this chat: keep it to yourself. (This page's code comes from Parley's servers, so that promise relies on Parley serving it honestly. And ${agentName()} reads the conversation to reply, as with any ${agentName()} chat.)`,
|
|
371
371
|
)
|
|
372
372
|
: null,
|
|
373
373
|
el("p", {}, `When the replies run out, the chat pauses until ${whom} adds more. Replies come from ${theirs} computer, so it needs to be on, and the link stops working for good once the chat ends.`),
|
|
@@ -430,7 +430,7 @@
|
|
|
430
430
|
function showEnded() {
|
|
431
431
|
// For the guest, one quiet line about having their own.
|
|
432
432
|
const own = () =>
|
|
433
|
-
owner ? null : el("p", { className: "own" }, "Want people to talk to your agent? ", el("a", { href: homeUrl, target: "_blank", rel: "noopener", textContent: "Get
|
|
433
|
+
owner ? null : el("p", { className: "own" }, "Want people to talk to your agent? ", el("a", { href: homeUrl, target: "_blank", rel: "noopener", textContent: "Get Parley" }));
|
|
434
434
|
showNote("This parley has ended", "Thanks for the feedback. The conversation isn't available here anymore.", own);
|
|
435
435
|
}
|
|
436
436
|
|
|
@@ -497,7 +497,7 @@
|
|
|
497
497
|
el("span", { className: "slash", textContent: "/" }),
|
|
498
498
|
el("span", { className: "here", textContent: `${s.guest} on ${s.project}` }),
|
|
499
499
|
);
|
|
500
|
-
document.title = `${s.guest} on ${s.project} ·
|
|
500
|
+
document.title = `${s.guest} on ${s.project} · Parley`;
|
|
501
501
|
const used = s.replyLimit - s.repliesLeft;
|
|
502
502
|
const pct = Math.min(100, Math.round((used / s.replyLimit) * 100));
|
|
503
503
|
const kv = (pairs) => el("dl", { className: "kv" }, ...pairs.flatMap(([k, v]) => [el("dt", { textContent: k }), el("dd", { textContent: v })]));
|
package/web/dashboard.js
CHANGED
|
@@ -52,7 +52,7 @@
|
|
|
52
52
|
const row = document.querySelector(`[data-computer="${computer}"]`);
|
|
53
53
|
if (row) {
|
|
54
54
|
row.querySelector(".dot").className = `dot${m.on ? " on" : ""}`;
|
|
55
|
-
row.querySelector(".computer-status").textContent = m.on ? `online now${m.version ? ` ·
|
|
55
|
+
row.querySelector(".computer-status").textContent = m.on ? `online now${m.version ? ` · Parley ${m.version}` : ""}` : "offline";
|
|
56
56
|
}
|
|
57
57
|
paint();
|
|
58
58
|
}
|
package/web/home.js
CHANGED
|
@@ -1,7 +1,9 @@
|
|
|
1
|
-
//
|
|
2
|
-
// a guest sees one
|
|
3
|
-
//
|
|
4
|
-
//
|
|
1
|
+
// Parley's front page: plays the demo, three short parleys in turn, the way
|
|
2
|
+
// a guest sees one. The header, the composer and the working mark (mark.js)
|
|
3
|
+
// are the real chat's. The first scene is already in the page, so without
|
|
4
|
+
// script (or for anyone who prefers less motion) it just sits there, and the
|
|
5
|
+
// tabs swap scenes without playing them. Picking a scene plays it, then
|
|
6
|
+
// stays on it for a while before the others come round again.
|
|
5
7
|
(() => {
|
|
6
8
|
const root = document.getElementById("demo");
|
|
7
9
|
const list = document.getElementById("demo-msgs");
|
|
@@ -9,7 +11,12 @@
|
|
|
9
11
|
if (!root || !list || !data) return;
|
|
10
12
|
const scenes = JSON.parse(data.textContent);
|
|
11
13
|
const tabs = [...root.querySelectorAll("[data-scene]")];
|
|
14
|
+
const composer = document.getElementById("demo-composer");
|
|
15
|
+
const input = document.getElementById("demo-input");
|
|
16
|
+
const send = document.getElementById("demo-send");
|
|
17
|
+
const chip = document.getElementById("demo-chip");
|
|
12
18
|
const still = matchMedia("(prefers-reduced-motion: reduce)").matches;
|
|
19
|
+
const STAY = 30_000; // after someone picks a scene
|
|
13
20
|
let run = 0; // a new run cancels the one playing
|
|
14
21
|
|
|
15
22
|
const el = (tag, className, text) => {
|
|
@@ -42,6 +49,33 @@
|
|
|
42
49
|
mark.classList.add("opening");
|
|
43
50
|
}
|
|
44
51
|
|
|
52
|
+
// Like the real chat, the frame stays put and the conversation scrolls.
|
|
53
|
+
const toEnd = () => (list.scrollTop = list.scrollHeight);
|
|
54
|
+
|
|
55
|
+
function setInput(text) {
|
|
56
|
+
input?.replaceChildren(text ? document.createTextNode(text) : el("span", "demo-placeholder", "Write a message"));
|
|
57
|
+
send?.classList.toggle("off", !text);
|
|
58
|
+
toEnd();
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
// The guest types into the composer and sends, as in the real chat.
|
|
62
|
+
async function say(text, id) {
|
|
63
|
+
composer?.classList.add("focus");
|
|
64
|
+
for (let k = 2; k < text.length; k += 2) {
|
|
65
|
+
setInput(text.slice(0, k));
|
|
66
|
+
await wait(30, id);
|
|
67
|
+
}
|
|
68
|
+
setInput(text);
|
|
69
|
+
await wait(450, id);
|
|
70
|
+
send?.classList.add("press");
|
|
71
|
+
await wait(130, id);
|
|
72
|
+
send?.classList.remove("press");
|
|
73
|
+
setInput("");
|
|
74
|
+
composer?.classList.remove("focus");
|
|
75
|
+
list.append(guest(text));
|
|
76
|
+
toEnd();
|
|
77
|
+
}
|
|
78
|
+
|
|
45
79
|
function pick(i) {
|
|
46
80
|
tabs.forEach((tab, j) => {
|
|
47
81
|
tab.classList.toggle("on", i === j);
|
|
@@ -59,10 +93,13 @@
|
|
|
59
93
|
const scene = scenes[i];
|
|
60
94
|
pick(i);
|
|
61
95
|
list.replaceChildren();
|
|
96
|
+
setInput("");
|
|
97
|
+
composer?.classList.remove("focus");
|
|
98
|
+
if (chip) chip.textContent = "100/100";
|
|
62
99
|
opening();
|
|
100
|
+
await wait(1200, id);
|
|
101
|
+
await say(scene.asks, id);
|
|
63
102
|
await wait(500, id);
|
|
64
|
-
list.append(guest(scene.asks));
|
|
65
|
-
await wait(900, id);
|
|
66
103
|
// Working: the mark reads (its core flickers), then writes (light runs
|
|
67
104
|
// around it), then the reply streams in.
|
|
68
105
|
const node = el("li", "msg agent");
|
|
@@ -70,6 +107,7 @@
|
|
|
70
107
|
body.innerHTML = `<span class="thinking reading" role="img" aria-label="Working">${window.parleyMark({ moving: true })}</span>`;
|
|
71
108
|
node.append(body);
|
|
72
109
|
list.append(node);
|
|
110
|
+
toEnd();
|
|
73
111
|
await wait(1600, id);
|
|
74
112
|
body.firstElementChild.classList.remove("reading");
|
|
75
113
|
await wait(1400, id);
|
|
@@ -78,23 +116,29 @@
|
|
|
78
116
|
const words = scene.says.split(" ");
|
|
79
117
|
for (let w = 1; w <= words.length; w++) {
|
|
80
118
|
p.textContent = words.slice(0, w).join(" ");
|
|
119
|
+
toEnd();
|
|
81
120
|
await wait(55, id);
|
|
82
121
|
}
|
|
83
122
|
p.classList.remove("cursor");
|
|
84
|
-
|
|
85
|
-
|
|
123
|
+
if (chip) chip.textContent = "99/100";
|
|
124
|
+
await wait(1600, id);
|
|
125
|
+
await say(scene.then, id);
|
|
86
126
|
await wait(4500, id);
|
|
87
127
|
}
|
|
88
128
|
|
|
89
|
-
async function loop(from) {
|
|
129
|
+
async function loop(from, stay = 0) {
|
|
90
130
|
const id = ++run;
|
|
91
131
|
try {
|
|
92
|
-
for (let i = from; ; i = (i + 1) % scenes.length)
|
|
132
|
+
for (let i = from; ; i = (i + 1) % scenes.length) {
|
|
133
|
+
await play(i, id);
|
|
134
|
+
if (stay) await wait(stay, id);
|
|
135
|
+
stay = 0;
|
|
136
|
+
}
|
|
93
137
|
} catch {
|
|
94
138
|
// Stopped for another scene.
|
|
95
139
|
}
|
|
96
140
|
}
|
|
97
141
|
|
|
98
|
-
tabs.forEach((tab, i) => tab.addEventListener("click", () => (still ? show(i) : loop(i))));
|
|
142
|
+
tabs.forEach((tab, i) => tab.addEventListener("click", () => (still ? show(i) : loop(i, STAY))));
|
|
99
143
|
if (!still) loop(0);
|
|
100
144
|
})();
|
package/web/panel.html
CHANGED
|
@@ -4,7 +4,7 @@
|
|
|
4
4
|
<meta charset="utf-8">
|
|
5
5
|
<meta name="viewport" content="width=device-width, initial-scale=1">
|
|
6
6
|
<meta name="theme-color" content="#0c0c0d">
|
|
7
|
-
<title>Control panel ·
|
|
7
|
+
<title>Control panel · Parley</title>
|
|
8
8
|
<link rel="icon" href="/static/icon.svg?v=6" type="image/svg+xml">
|
|
9
9
|
<link rel="stylesheet" href="/static/app.css">
|
|
10
10
|
</head>
|
package/web/panel.js
CHANGED
|
@@ -51,7 +51,7 @@
|
|
|
51
51
|
offline: ["warn", `Reconnecting to ${host}`],
|
|
52
52
|
"signed-out": ["warn", "Not signed in · run parley login"],
|
|
53
53
|
unlinked: ["off", "Unlinked · run parley login"],
|
|
54
|
-
replaced: ["off", "Another
|
|
54
|
+
replaced: ["off", "Another Parley server took over"],
|
|
55
55
|
};
|
|
56
56
|
const [dot, label] = RELAY[relay.state] ?? ["warn", relay.state];
|
|
57
57
|
$("server-dot").className = `dot ${dot}`;
|