@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 CHANGED
@@ -1,4 +1,4 @@
1
- # parley
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 parley account once:
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 parley starts it by its real path and lets it read that one file.
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, parley checks the agent is installed and new enough for these flags, and says what to update if not.
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 parley's sandbox. Update it (${UPDATE[agent]}) and try again.`;
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 ?? `parley's servers answered ${res.status}`);
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 = `parley: someone chats with a read-only Claude or Codex running in this project, on this
16
- computer, and you get everything they said. Good for feedback from guests, or any interview.
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 parley account
41
- parley start | stop | status The background server (holds the connection to parley)
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 parley's MCP tools to Claude Code and Codex; start at login
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 parley login live in ${paths.home}.`;
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 parley MCP server (user scope)." : "Claude Code: couldn't add the MCP server.");
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 parley MCP server." : "Codex: couldn't add the MCP server.");
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(`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.`);
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 parley session: a private link the user can send to a guest. The guest chats with a fresh, read-only " +
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 parley running.");
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 parley sessions, newest activity first, with how many guest messages are new since you last read each one. " +
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 parley session as Markdown: the brief, every message, which model interviewed, and paths to screenshots the " +
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 parley session for good when the user is done with it. The guest's link stops working entirely and an open " +
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 parley on this computer took over the connection." };
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} · parley`))
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(`parley: a server is already running on ${config.dashboardPort}; leaving it be.`);
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(`parley: control panel http://localhost:${config.dashboardPort}, local links on ${config.port}`);
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
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@parley-im/parley",
3
- "version": "0.2.10",
3
+ "version": "0.2.12",
4
4
  "description": "Send someone a link. They talk to a read-only Claude or Codex in your project, on your computer, and you get everything they said.",
5
5
  "keywords": [
6
6
  "claude",
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
- .docs { padding-bottom: 40px; }
460
- .docs h2 { font-size: 17px; letter-spacing: -0.01em; margin: 36px 0 10px; }
461
- .docs p, .docs li { max-width: 66ch; }
462
- .docs ul { padding-left: 1.2em; }
463
- .docs li { margin: 6px 0; }
464
- .docs pre { background: var(--subtle); border: 1px solid var(--line); border-radius: 8px; padding: 12px 14px; overflow-x: auto; font-size: 13px; line-height: 1.6; }
465
- .docs pre code { background: none; border: 0; padding: 0; font-size: inherit; }
466
- .docs blockquote { margin: 10px 0; padding: 8px 14px; border-left: 3px solid var(--accent); background: var(--accent-soft); border-radius: 0 8px 8px 0; color: var(--text); }
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
- .demo-chat { background: var(--panel); border: 1px solid var(--line); border-radius: 18px; box-shadow: var(--shadow); overflow: hidden; }
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 6px; min-height: 340px; display: flex; flex-direction: column; gap: 14px; }
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
- .demo-say { margin: 0 14px 14px; padding: 8px 8px 8px 14px; border: 1.5px solid var(--line-strong); border-radius: 13px; color: var(--faint); font-size: 15px; display: flex; align-items: center; justify-content: space-between; }
500
- .demo-say .send-dot { width: 32px; height: 32px; border-radius: 9px; background: var(--subtle); }
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-msgs { min-height: 400px; padding: 14px 12px 6px; } }
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="parley: let people talk to your agent">
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">&nbsp;</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="Say anything. Ramble away." autocomplete="off" autocorrect="on" spellcheck="true" aria-label="Message"></textarea>
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 ? `. parley checked it's really them, with Google.` : "."),
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. parley passes them along but can't read them; it knows who sent this link, the project's name and when messages go by. 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.)`,
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 parley" }));
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} · parley`;
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 ? ` · parley ${m.version}` : ""}` : "offline";
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
- // parley's front page: plays the demo, three short parleys in turn, the way
2
- // a guest sees one (mark.js draws the working mark). The first is already in the page, so without script (or
3
- // for anyone who prefers less motion) it just sits there, and the tabs swap
4
- // scenes without playing them.
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
- await wait(1500, id);
85
- list.append(guest(scene.then));
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) await play(i, id);
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 · parley</title>
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 parley took over"],
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}`;