@timqi/pier 0.0.28 → 0.1.0
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/agent/config-sync.js +12 -1
- package/dist/agent/config.js +53 -23
- package/dist/agent/events.js +27 -4
- package/dist/agent/listing.js +114 -16
- package/dist/agent/pi.js +158 -16
- package/dist/channels/lark-panel.js +1 -1
- package/dist/core/inbox.js +1 -1
- package/dist/core/router.js +14 -0
- package/dist/core/types.js +13 -1
- package/dist/db.js +48 -0
- package/dist/drain.js +1 -1
- package/dist/extensions/web/content.js +2 -2
- package/dist/main.js +8 -1
- package/dist/secrets.js +1 -1
- package/dist/service.js +2 -2
- package/dist/settings.js +31 -10
- package/dist/tasks/agent.js +109 -73
- package/dist/tasks/callbacks.js +1 -3
- package/dist/tasks/command.js +27 -8
- package/dist/tasks/execution.js +6 -4
- package/dist/tasks/groups.js +35 -26
- package/dist/tasks/messages.js +33 -27
- package/dist/tasks/outbox.js +48 -21
- package/dist/tasks/routes.js +1 -4
- package/dist/tasks/runs.js +10 -4
- package/dist/tasks/service.js +24 -12
- package/dist/tasks/store.js +17 -3
- package/dist/tasks/tool.js +97 -25
- package/dist/tools.js +2 -2
- package/dist/update.js +1 -1
- package/dist/web/instance.js +10 -3
- package/dist/web/providers.js +9 -2
- package/dist/web/public/assets/{activity-DVamP8fs.js → activity-Bl3vZukb.js} +2 -2
- package/dist/web/public/assets/activity-Bl3vZukb.js.br +0 -0
- package/dist/web/public/assets/activity-Bl3vZukb.js.gz +0 -0
- package/dist/web/public/assets/boards-DYuf4Mlj.js +1 -0
- package/dist/web/public/assets/boards-DYuf4Mlj.js.br +0 -0
- package/dist/web/public/assets/boards-DYuf4Mlj.js.gz +0 -0
- package/dist/web/public/assets/explorer-qJH_9nTE.js +4 -0
- package/dist/web/public/assets/explorer-qJH_9nTE.js.br +0 -0
- package/dist/web/public/assets/explorer-qJH_9nTE.js.gz +0 -0
- package/dist/web/public/assets/index-Dqdb-Eqt.js +85 -0
- package/dist/web/public/assets/index-Dqdb-Eqt.js.br +0 -0
- package/dist/web/public/assets/index-Dqdb-Eqt.js.gz +0 -0
- package/dist/web/public/assets/index-DzmMzvi_.css +2 -0
- package/dist/web/public/assets/index-DzmMzvi_.css.br +0 -0
- package/dist/web/public/assets/index-DzmMzvi_.css.gz +0 -0
- package/dist/web/public/assets/runs-BLJu7EXN.js +1 -0
- package/dist/web/public/assets/runs-BLJu7EXN.js.br +0 -0
- package/dist/web/public/assets/runs-BLJu7EXN.js.gz +0 -0
- package/dist/web/public/assets/settings-BrdVh-Zi.js +5 -0
- package/dist/web/public/assets/settings-BrdVh-Zi.js.br +0 -0
- package/dist/web/public/assets/settings-BrdVh-Zi.js.gz +0 -0
- package/dist/web/public/assets/task-runs-CeQS1rxa.js +3 -0
- package/dist/web/public/assets/task-runs-CeQS1rxa.js.br +0 -0
- package/dist/web/public/assets/task-runs-CeQS1rxa.js.gz +0 -0
- package/dist/web/public/assets/tasks-bcb3fYdK.js +4 -0
- package/dist/web/public/assets/tasks-bcb3fYdK.js.br +0 -0
- package/dist/web/public/assets/tasks-bcb3fYdK.js.gz +0 -0
- package/dist/web/public/index.html +103 -134
- package/dist/web/public/index.html.br +0 -0
- package/dist/web/public/index.html.gz +0 -0
- package/dist/web/public/manifest.webmanifest +2 -2
- package/dist/web/public/manifest.webmanifest.br +0 -0
- package/dist/web/public/manifest.webmanifest.gz +0 -0
- package/dist/web/server.js +55 -36
- package/dist/web/session-state.js +54 -33
- package/package.json +2 -1
- package/skills/pier-tasks/SKILL.md +146 -127
- package/dist/web/public/assets/activity-DVamP8fs.js.br +0 -0
- package/dist/web/public/assets/activity-DVamP8fs.js.gz +0 -0
- package/dist/web/public/assets/boards-BKtjPwus.js +0 -1
- package/dist/web/public/assets/boards-BKtjPwus.js.br +0 -0
- package/dist/web/public/assets/boards-BKtjPwus.js.gz +0 -0
- package/dist/web/public/assets/explorer-CjO9NAKQ.js +0 -4
- package/dist/web/public/assets/explorer-CjO9NAKQ.js.br +0 -0
- package/dist/web/public/assets/explorer-CjO9NAKQ.js.gz +0 -0
- package/dist/web/public/assets/index-C23sXyS3.css +0 -2
- package/dist/web/public/assets/index-C23sXyS3.css.br +0 -0
- package/dist/web/public/assets/index-C23sXyS3.css.gz +0 -0
- package/dist/web/public/assets/index-DzWnvhXb.js +0 -85
- package/dist/web/public/assets/index-DzWnvhXb.js.br +0 -0
- package/dist/web/public/assets/index-DzWnvhXb.js.gz +0 -0
- package/dist/web/public/assets/runs-CxIZAx1s.js +0 -1
- package/dist/web/public/assets/runs-CxIZAx1s.js.br +0 -0
- package/dist/web/public/assets/runs-CxIZAx1s.js.gz +0 -0
- package/dist/web/public/assets/settings-B8JWLbpL.js +0 -5
- package/dist/web/public/assets/settings-B8JWLbpL.js.br +0 -0
- package/dist/web/public/assets/settings-B8JWLbpL.js.gz +0 -0
- package/dist/web/public/assets/task-runs-BZ1brkLC.js +0 -3
- package/dist/web/public/assets/task-runs-BZ1brkLC.js.br +0 -0
- package/dist/web/public/assets/task-runs-BZ1brkLC.js.gz +0 -0
- package/dist/web/public/assets/tasks-BoYiBQpq.js +0 -4
- package/dist/web/public/assets/tasks-BoYiBQpq.js.br +0 -0
- package/dist/web/public/assets/tasks-BoYiBQpq.js.gz +0 -0
|
@@ -3,19 +3,23 @@
|
|
|
3
3
|
<head>
|
|
4
4
|
<meta charset="utf-8" />
|
|
5
5
|
<!-- viewport-fit=cover lets the safe-area insets in style.css do their job
|
|
6
|
-
under an iPhone's home indicator.
|
|
7
|
-
|
|
8
|
-
|
|
6
|
+
under an iPhone's home indicator. No user scaling: a fixed-viewport
|
|
7
|
+
workbench zoomed by a stray gesture has no visible way back — said again
|
|
8
|
+
as `touch-action` (style.css) for the engines that ignore this pair. -->
|
|
9
|
+
<meta name="viewport" content="width=device-width, initial-scale=1, maximum-scale=1, user-scalable=no, viewport-fit=cover" />
|
|
10
|
+
<meta name="theme-color" content="#f1f4f3" />
|
|
9
11
|
<!-- Ahead of the stylesheet, because a workbench that flashes white and then
|
|
10
12
|
goes dark reads as a bug. This is the only place "system" is resolved
|
|
11
13
|
before the bundle exists; theme.ts owns the choice from there on. -->
|
|
12
14
|
<script>
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
15
|
+
var t;
|
|
16
|
+
try { t = localStorage.getItem("pier.theme"); }
|
|
17
|
+
catch (e) { /* storage denied: follow the system for this visit */ }
|
|
18
|
+
var mode = t === "light" || t === "dark" ? t
|
|
19
|
+
: matchMedia("(prefers-color-scheme: dark)").matches ? "dark" : "light";
|
|
20
|
+
document.documentElement.dataset.theme = mode;
|
|
21
|
+
// Before CSS loads, mirror its canvas token so installed chrome never flashes.
|
|
22
|
+
document.querySelector('meta[name="theme-color"]').content = mode === "dark" ? "#14171a" : "#f1f4f3";
|
|
19
23
|
</script>
|
|
20
24
|
<!-- Installed as a home-screen app: standalone window, own icon. The
|
|
21
25
|
manifest shares the workbench's cookie boundary. -->
|
|
@@ -33,8 +37,8 @@
|
|
|
33
37
|
which style.css pays for with the safe-area inset. -->
|
|
34
38
|
<meta name="apple-mobile-web-app-status-bar-style" content="black-translucent" />
|
|
35
39
|
<title>Pier</title>
|
|
36
|
-
<script type="module" crossorigin src="/assets/index-
|
|
37
|
-
<link rel="stylesheet" crossorigin href="/assets/index-
|
|
40
|
+
<script type="module" crossorigin src="/assets/index-Dqdb-Eqt.js"></script>
|
|
41
|
+
<link rel="stylesheet" crossorigin href="/assets/index-DzmMzvi_.css">
|
|
38
42
|
</head>
|
|
39
43
|
<!-- The document never scrolls: this is a fixed-viewport workbench, and every
|
|
40
44
|
scrollable region is an inner pane. h-dvh, not h-screen, because 100vh can
|
|
@@ -50,16 +54,14 @@
|
|
|
50
54
|
control in the same band. -->
|
|
51
55
|
<button type="button" id="rail-toggle" aria-label="Toggle sidebar"
|
|
52
56
|
class="fixed top-1.5 z-40 hidden h-7 w-7 cursor-pointer items-center justify-center rounded-md text-neutral-400 hover:bg-neutral-200 hover:text-neutral-700 md:flex">
|
|
53
|
-
<
|
|
54
|
-
<path d="M9.5 4 5.5 8l4 4" stroke-linecap="round" stroke-linejoin="round" />
|
|
55
|
-
</svg>
|
|
57
|
+
<span data-icon="ChevronLeft" class="h-3.5 w-3.5"></span>
|
|
56
58
|
</button>
|
|
57
|
-
<aside id="sidebar" class="flex flex-col overflow-hidden border-r border-neutral-200 bg-neutral-50">
|
|
58
|
-
<header class="flex items-center gap-2 px-3
|
|
59
|
+
<aside id="sidebar" class="flex flex-col overflow-hidden border-r border-neutral-200 bg-neutral-50 font-sans">
|
|
60
|
+
<header class="flex h-12 flex-none items-center gap-2 px-3">
|
|
59
61
|
<!-- The same mark as the tab and the home-screen icon, so an installed
|
|
60
62
|
Pier looks like the thing you installed. -->
|
|
61
|
-
<img src="/icon.svg" alt="" width="20" height="20" class="h-
|
|
62
|
-
<h1 class="text-[
|
|
63
|
+
<img src="/icon.svg" alt="" width="20" height="20" class="h-7 w-7 flex-none rounded-lg border border-neutral-200/70 bg-white p-1 shadow-xs" />
|
|
64
|
+
<h1 class="text-[17px] font-semibold tracking-tight">Pier</h1>
|
|
63
65
|
<!-- A link, not a label: AGPL-3.0 section 13 asks a network service to
|
|
64
66
|
point its users at the source it is running. Beside the title
|
|
65
67
|
because a version is something you look up, not something you find
|
|
@@ -74,72 +76,59 @@
|
|
|
74
76
|
<button type="button" id="theme-toggle" aria-label="Theme"
|
|
75
77
|
class="icon-btn text-[13px]"></button>
|
|
76
78
|
</header>
|
|
79
|
+
<div class="flex flex-none items-center gap-1.5 px-3 pb-1">
|
|
80
|
+
<button type="button" id="new-session" aria-label="New session"
|
|
81
|
+
class="btn btn-primary flex min-w-0 flex-1 items-center justify-center gap-2 whitespace-nowrap px-3">
|
|
82
|
+
<span data-icon="Plus" aria-hidden="true" class="h-3.5 w-3.5"></span> New session
|
|
83
|
+
</button>
|
|
84
|
+
<button type="button" id="open-palette" aria-label="Search sessions, messages and Console"
|
|
85
|
+
class="btn flex h-8 w-8 flex-none items-center justify-center bg-neutral-100 p-0 text-neutral-600 hover:bg-neutral-200 active:bg-neutral-300">
|
|
86
|
+
<span data-icon="Search" class="h-5 w-5 flex-none"></span>
|
|
87
|
+
<span class="sr-only">Search</span>
|
|
88
|
+
</button>
|
|
89
|
+
</div>
|
|
77
90
|
<details id="console-section" open class="flex-none border-b border-neutral-200/70">
|
|
78
|
-
<summary class="flex cursor-pointer select-none items-center gap-1 px-3
|
|
79
|
-
<span class="chev"
|
|
91
|
+
<summary class="flex cursor-pointer select-none items-center gap-1 px-3 py-1 text-xs font-semibold leading-5 text-neutral-500 hover:bg-neutral-100">
|
|
92
|
+
<span data-icon="ChevronRight" class="chev h-3 w-3"></span>Console
|
|
80
93
|
</summary>
|
|
81
|
-
<ul class="pb-1
|
|
94
|
+
<ul class="pb-1">
|
|
82
95
|
<!-- One row for the tasks area: Tasks, Runs and Activity are tabs of
|
|
83
96
|
the Automation page (views.ts), not three sidebar entries. -->
|
|
84
97
|
<li><button type="button" id="open-tasks"
|
|
85
|
-
class="flex w-full cursor-pointer items-center gap-2
|
|
86
|
-
<
|
|
87
|
-
<path d="M6 3h8M6 8h8M6 13h8M2 3h1M2 8h1M2 13h1" />
|
|
88
|
-
</svg>Automation</button></li>
|
|
98
|
+
class="flex w-full cursor-pointer items-center gap-2 pr-3 text-left hover:bg-neutral-100">
|
|
99
|
+
<span data-icon="ListTodo" class="h-3.5 w-3.5 flex-none text-neutral-400"></span>Automation</button></li>
|
|
89
100
|
<li><button type="button" id="open-boards"
|
|
90
|
-
class="flex w-full cursor-pointer items-center gap-2
|
|
91
|
-
<
|
|
92
|
-
<rect x="2" y="2" width="12" height="12" rx="1.5" /><path d="M2 6.5h12M6.5 6.5V14" />
|
|
93
|
-
</svg>Boards</button></li>
|
|
101
|
+
class="flex w-full cursor-pointer items-center gap-2 pr-3 text-left hover:bg-neutral-100">
|
|
102
|
+
<span data-icon="LayoutDashboard" class="h-3.5 w-3.5 flex-none text-neutral-400"></span>Boards</button></li>
|
|
94
103
|
<li><button type="button" id="open-settings"
|
|
95
|
-
class="flex w-full cursor-pointer items-center gap-2
|
|
96
|
-
<
|
|
97
|
-
<!-- Teeth every 60° about (8,8); the hub is that same centre. -->
|
|
98
|
-
<path d="M6.8 2.2h2.4l.32 1.6a4.9 4.9 0 0 1 1.36.79l1.55-.53 1.2 2.08-1.23 1.07a4.9 4.9 0 0 1 0 1.58l1.23 1.07-1.2 2.08-1.55-.53a4.9 4.9 0 0 1-1.36.79l-.32 1.6H6.8l-.32-1.6a4.9 4.9 0 0 1-1.36-.79l-1.55.53-1.2-2.08 1.23-1.07a4.9 4.9 0 0 1 0-1.58L2.37 6.14l1.2-2.08 1.55.53a4.9 4.9 0 0 1 1.36-.79z" stroke-linejoin="round" />
|
|
99
|
-
<circle cx="8" cy="8" r="1.9" />
|
|
100
|
-
</svg>Settings</button></li>
|
|
104
|
+
class="flex w-full cursor-pointer items-center gap-2 pr-3 text-left hover:bg-neutral-100">
|
|
105
|
+
<span data-icon="Settings" class="h-3.5 w-3.5 flex-none text-neutral-400"></span>Settings</button></li>
|
|
101
106
|
</ul>
|
|
102
107
|
</details>
|
|
103
|
-
<div class="flex
|
|
104
|
-
|
|
105
|
-
<button type="button" id="open-archive"
|
|
106
|
-
class="icon-btn ml-auto" aria-label="Search sessions and Console">
|
|
107
|
-
<svg viewBox="0 0 16 16" fill="none" stroke="currentColor" stroke-width="1.5" class="h-3.5 w-3.5">
|
|
108
|
-
<circle cx="7" cy="7" r="4.25" /><path d="M10.2 10.2 14 14" stroke-linecap="round" />
|
|
109
|
-
</svg>
|
|
110
|
-
</button>
|
|
111
|
-
<button type="button" id="new-session" title="New session"
|
|
112
|
-
class="icon-btn" aria-label="New session">
|
|
113
|
-
<svg viewBox="0 0 16 16" fill="none" stroke="currentColor" stroke-width="1.5" class="h-3.5 w-3.5">
|
|
114
|
-
<rect x="2.25" y="2.25" width="11.5" height="11.5" rx="2.5" />
|
|
115
|
-
<path d="M8 5.5v5M5.5 8h5" stroke-linecap="round" />
|
|
116
|
-
</svg>
|
|
117
|
-
</button>
|
|
118
|
-
</div>
|
|
119
|
-
<div id="session-list" class="flex-1 overflow-y-auto text-[13.5px]"></div>
|
|
108
|
+
<div class="flex-none px-3 pb-1 pt-2 text-xs font-semibold leading-5 text-neutral-500">Sessions</div>
|
|
109
|
+
<div id="session-list" class="min-h-0 flex-1 overflow-y-auto"></div>
|
|
120
110
|
</aside>
|
|
121
111
|
|
|
122
|
-
<main class="
|
|
112
|
+
<main class="min-w-0 overflow-hidden">
|
|
123
113
|
<!-- Mobile top bar: replaces the chat header below md (which hides), so a
|
|
124
114
|
Console view keeps the same way back to the drawer as a chat does. -->
|
|
125
|
-
<header id="mobile-bar" class="flex
|
|
115
|
+
<header id="mobile-bar" class="flex flex-none items-center gap-1 px-1.5 md:hidden">
|
|
126
116
|
<button type="button" id="drawer-toggle" aria-label="Open sidebar"
|
|
127
|
-
class="relative
|
|
128
|
-
<
|
|
129
|
-
<path d="M2.5 4h11M2.5 8h11M2.5 12h11" />
|
|
130
|
-
</svg>
|
|
117
|
+
class="icon-btn relative h-11 w-11 text-neutral-500">
|
|
118
|
+
<span data-icon="Menu" class="h-4.5 w-4.5"></span>
|
|
131
119
|
</button>
|
|
132
120
|
<span id="mobile-title" class="min-w-0 flex-1 truncate text-[16px] font-medium"></span>
|
|
121
|
+
<!-- Both bar controls are the shared round icon button at a thumb's size
|
|
122
|
+
(44px), with the chat header's own 14px ⋯ glyph — not a row of
|
|
123
|
+
oversized dots. -->
|
|
133
124
|
<button type="button" id="mobile-menu" aria-label="Session actions"
|
|
134
|
-
class="hidden h-
|
|
125
|
+
class="icon-btn hidden h-11 w-11"><span data-icon="Ellipsis" class="h-3.5 w-3.5"></span></button>
|
|
135
126
|
</header>
|
|
136
|
-
<!-- Automation hosts three views under one title and
|
|
137
|
-
|
|
127
|
+
<!-- Automation hosts three views under one head — title, tabs and the
|
|
128
|
+
page's primary action on one strip, drawn by views.ts; its active pill
|
|
129
|
+
tracks the route (#/tasks, #/runs, #/activity). -->
|
|
138
130
|
<section id="automation-view" class="hidden min-h-0 flex-1 flex-col">
|
|
139
|
-
<header
|
|
140
|
-
<span class="font-medium">Automation</span>
|
|
141
|
-
</header>
|
|
142
|
-
<div id="automation-tabs" class="tabstrip bg-white"></div>
|
|
131
|
+
<header id="automation-tabs" class="pagehead"></header>
|
|
143
132
|
<section id="tasks-view" class="hidden min-h-0 flex-1 flex-col"></section>
|
|
144
133
|
<section id="runs-view" class="hidden min-h-0 flex-1 flex-col"></section>
|
|
145
134
|
<section id="activity-view" class="hidden min-h-0 flex-1 flex-col"></section>
|
|
@@ -155,45 +144,30 @@
|
|
|
155
144
|
<!-- Inline session meta: model · reasoning · current context size. -->
|
|
156
145
|
<div id="session-meta" class="hidden flex-none items-center gap-1 text-[11px]"></div>
|
|
157
146
|
<button type="button" id="chat-menu" title="Session actions" aria-label="Session actions"
|
|
158
|
-
class="icon-btn"
|
|
147
|
+
class="icon-btn"><span data-icon="Ellipsis" class="h-3.5 w-3.5"></span></button>
|
|
159
148
|
</div>
|
|
160
149
|
</header>
|
|
161
150
|
<!-- The transcript's own type scale, inherited by rows, chips and markdown. -->
|
|
162
151
|
<!-- data-gallery: the lightbox pages within the container it was opened
|
|
163
152
|
from (see attachments.ts), so transcript and pending strip stay apart. -->
|
|
164
|
-
<div id="turns" data-gallery class="flex-1 overflow-y-auto pb-
|
|
153
|
+
<div id="turns" data-gallery class="flex-1 overflow-y-auto pb-5 pt-2 text-[15px] leading-[1.55]"></div>
|
|
165
154
|
<!-- Pi's queue primitives are whole-queue (steer / clear), so the actions
|
|
166
155
|
live on the header row and read as "the queue", never per message. -->
|
|
167
|
-
<div id="queue-panel" class="hidden flex-col border-t border-neutral-200 px-3
|
|
156
|
+
<div id="queue-panel" class="hidden flex-col border-t border-neutral-200 px-4 pb-3 pt-2">
|
|
168
157
|
<div class="flex h-7 items-center gap-2">
|
|
169
158
|
<span id="queue-label" class="text-[11px] font-semibold uppercase tracking-wide text-amber-700">Queued</span>
|
|
170
159
|
<div class="ml-auto flex items-center gap-0.5 text-amber-700">
|
|
171
160
|
<button type="button" id="queue-steer" title="Send the whole queue now into the running turn" aria-label="Send queue now"
|
|
172
161
|
class="flex h-7 w-7 cursor-pointer items-center justify-center rounded-md hover:bg-amber-100 active:bg-amber-200">
|
|
173
|
-
<
|
|
174
|
-
stroke-linecap="round" stroke-linejoin="round" class="h-3.5 w-3.5">
|
|
175
|
-
<path d="M14.5 1.5 7 9m7.5-7.5-4.2 12.6L7 9 1.7 6.7l12.8-5.2z" />
|
|
176
|
-
</svg>
|
|
162
|
+
<span data-icon="Send" class="h-3.5 w-3.5"></span>
|
|
177
163
|
</button>
|
|
178
164
|
<button type="button" id="queue-restart" title="Stop the running turn and send the whole queue as a new prompt" aria-label="Stop and send queue"
|
|
179
165
|
class="flex h-7 w-7 cursor-pointer items-center justify-center rounded-md hover:bg-amber-100 active:bg-amber-200">
|
|
180
|
-
<
|
|
181
|
-
stroke-linecap="round" stroke-linejoin="round" class="h-3.5 w-3.5">
|
|
182
|
-
<!-- The arc's ink sits high-right of the box; centre it so the
|
|
183
|
-
three icons share one baseline. -->
|
|
184
|
-
<g transform="translate(-0.8 1.9)">
|
|
185
|
-
<path d="M13 5.2V1.8m0 0H9.6M13 1.8A6 6 0 1 0 14 9" />
|
|
186
|
-
<path d="M6.2 8h4M8.5 5.7 10.8 8l-2.3 2.3" />
|
|
187
|
-
</g>
|
|
188
|
-
</svg>
|
|
166
|
+
<span data-icon="RotateCw" class="h-3.5 w-3.5"></span>
|
|
189
167
|
</button>
|
|
190
168
|
<button type="button" id="queue-recall" title="Return queued messages to the composer" aria-label="Recall queued messages"
|
|
191
169
|
class="flex h-7 w-7 cursor-pointer items-center justify-center rounded-md hover:bg-amber-100 active:bg-amber-200">
|
|
192
|
-
<
|
|
193
|
-
stroke-linecap="round" stroke-linejoin="round" class="h-3.5 w-3.5">
|
|
194
|
-
<!-- Ink centred on the 16×16 box, or the glyph sits low next to its neighbours. -->
|
|
195
|
-
<path d="M5 3 2 6l3 3M2.5 6H10a3.5 3.5 0 0 1 0 7H8" />
|
|
196
|
-
</svg>
|
|
170
|
+
<span data-icon="Undo2" class="h-3.5 w-3.5"></span>
|
|
197
171
|
</button>
|
|
198
172
|
</div>
|
|
199
173
|
</div>
|
|
@@ -201,82 +175,77 @@
|
|
|
201
175
|
(18px leading) rather than shrinking the turns pane any further. -->
|
|
202
176
|
<ul id="queue-rows" class="flex max-h-[6rem] flex-col gap-1 overflow-y-auto overscroll-contain"></ul>
|
|
203
177
|
</div>
|
|
204
|
-
<form id="composer" class="
|
|
205
|
-
<div class="rounded-
|
|
178
|
+
<form id="composer" class="flex-none px-4 pb-4 pt-2">
|
|
179
|
+
<div class="glass rounded-[1.75rem] border border-neutral-200 px-3 py-2.5 transition-shadow focus-within:border-indigo-400">
|
|
206
180
|
<div id="image-strip" data-gallery class="hidden flex-wrap gap-2 px-1 pb-1.5 pt-1"></div>
|
|
207
181
|
<!-- items-end keeps the icons on the bottom row while the textarea grows upward -->
|
|
208
182
|
<div class="flex items-end gap-1.5">
|
|
209
183
|
<button type="button" id="attach" title="Attach file (or paste / drop)"
|
|
210
|
-
class="flex h-7 w-7 flex-none cursor-pointer items-center justify-center rounded-lg text-[17px] leading-none text-neutral-400 hover:bg-neutral-100 hover:text-neutral-600"
|
|
184
|
+
class="flex h-7 w-7 flex-none cursor-pointer items-center justify-center rounded-lg text-[17px] leading-none text-neutral-400 hover:bg-neutral-100 hover:text-neutral-600" aria-label="Attach file"><span data-icon="Plus" class="h-3.5 w-3.5"></span></button>
|
|
211
185
|
<input id="attach-input" type="file" multiple class="hidden" />
|
|
212
186
|
<textarea id="input" rows="1"
|
|
213
187
|
class="block max-h-48 min-w-0 flex-1 resize-none bg-transparent px-1 py-0.5 focus:outline-none"
|
|
214
188
|
placeholder="Message…"></textarea>
|
|
215
|
-
<button type="button" id="stop" title="Stop the running turn"
|
|
189
|
+
<button type="button" id="stop" title="Stop the running turn" aria-label="Stop the running turn"
|
|
216
190
|
class="hidden h-7 w-7 flex-none cursor-pointer items-center justify-center rounded-lg bg-red-50 text-red-600 hover:bg-red-100">
|
|
217
|
-
<
|
|
218
|
-
<rect x="3.5" y="3.5" width="9" height="9" rx="1.5" />
|
|
219
|
-
</svg>
|
|
191
|
+
<span data-icon="Square" class="h-3.5 w-3.5"></span>
|
|
220
192
|
</button>
|
|
221
193
|
<!-- Style + icon double as the state display: indigo plane = send, amber clock = queue -->
|
|
222
194
|
<button type="submit" id="send" title="Send">
|
|
223
|
-
<
|
|
224
|
-
|
|
225
|
-
<path d="M14.5 1.5 7 9m7.5-7.5-4.2 12.6L7 9 1.7 6.7l12.8-5.2z" />
|
|
226
|
-
</svg>
|
|
227
|
-
<svg id="send-queue" viewBox="0 0 16 16" fill="none" stroke="currentColor"
|
|
228
|
-
stroke-width="1.5" stroke-linecap="round" class="hidden h-3.5 w-3.5">
|
|
229
|
-
<circle cx="8" cy="8" r="6" />
|
|
230
|
-
<path d="M8 4.75V8l2.4 1.5" />
|
|
231
|
-
</svg>
|
|
195
|
+
<span id="send-plane" data-icon="Send" class="h-3.5 w-3.5"></span>
|
|
196
|
+
<span id="send-queue" data-icon="Clock" class="hidden h-3.5 w-3.5"></span>
|
|
232
197
|
</button>
|
|
233
198
|
</div>
|
|
234
199
|
</div>
|
|
235
200
|
</form>
|
|
236
201
|
</main>
|
|
237
202
|
|
|
238
|
-
<dialog id="new-dialog" class="m-auto w-[min(24rem,92vw)] rounded-xl border border-neutral-200 p-4 shadow-xl backdrop:bg-black/20">
|
|
239
|
-
<form id="new-form" method="dialog" class="flex flex-col gap-3">
|
|
240
|
-
<h3 class="font-semibold">New session</h3>
|
|
241
|
-
<label class="flex flex-col gap-1 text-[13px] text-neutral-600">
|
|
242
|
-
Working directory
|
|
243
|
-
<input id="new-cwd" type="text" required placeholder="/path/to/project"
|
|
244
|
-
class="w-full rounded-md border border-neutral-300 px-2 py-1.5 font-mono text-[13px] focus:border-indigo-400 focus:outline-none" />
|
|
245
|
-
</label>
|
|
246
|
-
<div class="flex justify-end gap-2">
|
|
247
|
-
<button type="button" id="new-cancel" class="btn text-[13px]">Cancel</button>
|
|
248
|
-
<button type="submit" class="btn btn-primary text-[13px]">Create</button>
|
|
249
|
-
</div>
|
|
250
|
-
</form>
|
|
251
|
-
</dialog>
|
|
252
|
-
|
|
253
203
|
<!-- Not m-auto: a palette that grows downward as you type should not also
|
|
254
204
|
drift upward to stay centred. Pinned near the top instead, which is
|
|
255
|
-
where the eye already is and where every other ⌘K lives.
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
205
|
+
where the eye already is and where every other ⌘K lives. A solid
|
|
206
|
+
surface (docs/design/06-ui-ux.md): glass is for chrome, and this is
|
|
207
|
+
something you read. The edge and shadow tokens are set in style.css;
|
|
208
|
+
a click on the backdrop lands on the dialog element itself and closes
|
|
209
|
+
it (palette.ts). -->
|
|
210
|
+
<dialog id="palette" class="mx-auto mb-auto mt-[12vh] w-[36rem] max-w-[92vw] rounded-3xl border p-0 backdrop:bg-black/25">
|
|
211
|
+
<div class="flex h-[3.25rem] items-center gap-3 border-b border-neutral-200/70 px-4">
|
|
212
|
+
<span data-icon="Search" aria-hidden="true" class="h-[1.125rem] w-[1.125rem] flex-none text-neutral-400"></span>
|
|
213
|
+
<input id="palette-input" type="text" placeholder="Search sessions, messages and Console…" autocomplete="off" spellcheck="false"
|
|
214
|
+
class="min-w-0 flex-1 bg-transparent text-[1.0625rem] leading-6 placeholder:text-neutral-400 focus:outline-none" />
|
|
215
|
+
<kbd class="flex-none rounded-md border border-neutral-200 px-1.5 font-sans text-[0.6875rem] leading-5 text-neutral-500">esc</kbd>
|
|
216
|
+
</div>
|
|
217
|
+
<ul id="palette-list" role="listbox" aria-label="Results" class="max-h-[60vh] overflow-y-auto px-2 pb-2 text-[0.9375rem]"></ul>
|
|
218
|
+
<div class="flex items-center justify-between border-t border-neutral-200/70 px-4 py-1.5 text-[0.75rem] leading-4 text-neutral-500">
|
|
219
|
+
<span id="palette-keys">↑↓ navigate · ↵ open</span>
|
|
220
|
+
<span id="palette-count"></span>
|
|
262
221
|
</div>
|
|
263
|
-
<ul id="archive-list" class="max-h-[60vh] overflow-y-auto py-1 text-[13.5px]"></ul>
|
|
264
222
|
</dialog>
|
|
265
223
|
|
|
266
|
-
<!-- Full-size view of a thumbnail; click
|
|
267
|
-
The arrows page through the gallery it was
|
|
268
|
-
they hide when there is only one image to look
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
224
|
+
<!-- Full-size view of a thumbnail; a single click (or Esc) closes, a double
|
|
225
|
+
one zooms about the point. The arrows page through the gallery it was
|
|
226
|
+
opened from, as do ← / →; they hide when there is only one image to look
|
|
227
|
+
at. The dialog fills the viewport rather than hugging the image, so the
|
|
228
|
+
arrows sit at the screen's edges and a tall shot and a wide one don't
|
|
229
|
+
move the control under your thumb; #image-stage is the box a zoomed
|
|
230
|
+
image scrolls inside (ui/attachments.ts). -->
|
|
231
|
+
<!-- The size is written out, not left to `inset-0`: a dialog is `width:
|
|
232
|
+
fit-content` in the UA sheet, and an inset pair does not stretch a box
|
|
233
|
+
that already has a width — it sized itself to the image and sat in the
|
|
234
|
+
top-left corner, with the backdrop clicks landing on the dialog instead
|
|
235
|
+
of on #image-stage. -->
|
|
236
|
+
<dialog id="image-dialog" class="fixed inset-0 m-0 h-[100dvh] w-full max-h-none max-w-none bg-transparent p-0 backdrop:bg-black/70">
|
|
237
|
+
<div id="image-stage" class="flex h-full w-full items-center justify-center overflow-hidden">
|
|
238
|
+
<img id="image-full" alt="" class="max-h-[92dvh] max-w-[92vw] cursor-zoom-in rounded-xl object-contain" />
|
|
274
239
|
</div>
|
|
240
|
+
<!-- Siblings of the stage, not children of it: the stage is what scrolls
|
|
241
|
+
when zoomed, and an arrow inside it would scroll away with the image. -->
|
|
242
|
+
<button type="button" id="image-prev" aria-label="Previous image" class="lightbox-nav left-2"><span data-icon="ChevronLeft" class="h-6 w-6"></span></button>
|
|
243
|
+
<button type="button" id="image-next" aria-label="Next image" class="lightbox-nav right-2"><span data-icon="ChevronRight" class="h-6 w-6"></span></button>
|
|
275
244
|
</dialog>
|
|
276
245
|
|
|
277
246
|
<!-- Text preview of an agent attachment — the Files view's code pane fills
|
|
278
247
|
#file-text; download stays one click away. -->
|
|
279
|
-
<dialog id="file-dialog" class="m-auto w-[45rem] max-w-[92vw] rounded-
|
|
248
|
+
<dialog id="file-dialog" class="m-auto w-[45rem] max-w-[92vw] rounded-3xl border border-neutral-200 p-0 shadow-xl backdrop:bg-black/40">
|
|
280
249
|
<div class="flex items-center gap-2 border-b border-neutral-200 px-3 py-2">
|
|
281
250
|
<span id="file-name" class="min-w-0 flex-1 truncate font-mono text-[12.5px] text-neutral-700"></span>
|
|
282
251
|
<a id="file-download" download class="btn flex-none text-[12px] no-underline">Download</a>
|
|
Binary file
|
|
Binary file
|
|
@@ -8,8 +8,8 @@
|
|
|
8
8
|
"start_url": "/",
|
|
9
9
|
"scope": "/",
|
|
10
10
|
"display": "standalone",
|
|
11
|
-
"background_color": "#
|
|
12
|
-
"theme_color": "#
|
|
11
|
+
"background_color": "#f1f4f3",
|
|
12
|
+
"theme_color": "#f1f4f3",
|
|
13
13
|
"categories": ["developer", "productivity", "utilities"],
|
|
14
14
|
"launch_handler": { "client_mode": "focus-existing" },
|
|
15
15
|
"icons": [
|
|
Binary file
|
|
Binary file
|
package/dist/web/server.js
CHANGED
|
@@ -76,6 +76,15 @@ export function createServer({ factory, router, hub, sessions: state, config, pr
|
|
|
76
76
|
// every client shows the same attention state. Streaming → idle is the
|
|
77
77
|
// trigger — same transition the client notification uses — and it needs a
|
|
78
78
|
// start we witnessed, so a session that boots idle stays untouched.
|
|
79
|
+
//
|
|
80
|
+
// Only for the workbench's own sessions: an IM turn was already delivered to
|
|
81
|
+
// the chat it came from and a subagent's to its supervisor by callback, so no
|
|
82
|
+
// look here is owed for either — and none could clear the mark either, since
|
|
83
|
+
// the ack needs the session on screen. Marked anyway, they were a flag that
|
|
84
|
+
// only ever accumulated, and every reader had to subtract them again. The two
|
|
85
|
+
// facts are the ones the list already draws a row from (`present` below): no
|
|
86
|
+
// durable conversation row, and not a session a run made for itself.
|
|
87
|
+
const workbenchOwn = (id) => (channelOf?.(id) ?? "web") === "web" && !(taskSessions?.().has(id) ?? false);
|
|
79
88
|
const runningNow = new Set();
|
|
80
89
|
hub.subscribeWorkspace((e) => {
|
|
81
90
|
if (e.type !== "session-state")
|
|
@@ -86,6 +95,8 @@ export function createServer({ factory, router, hub, sessions: state, config, pr
|
|
|
86
95
|
}
|
|
87
96
|
if (!runningNow.delete(e.sessionId))
|
|
88
97
|
return;
|
|
98
|
+
if (!workbenchOwn(e.sessionId))
|
|
99
|
+
return;
|
|
89
100
|
state.setUnread(e.sessionId, true);
|
|
90
101
|
hub.emitWorkspace({ type: "sessions-changed" });
|
|
91
102
|
});
|
|
@@ -103,7 +114,7 @@ export function createServer({ factory, router, hub, sessions: state, config, pr
|
|
|
103
114
|
* not survive a restart or an eviction (Pi persisted nothing), but while
|
|
104
115
|
* this process lives it is in `nascent` and therefore in the rail — left
|
|
105
116
|
* alone, clicking it 404s forever. The load path is where a ghost is
|
|
106
|
-
* discovered, so it is where the entry and its
|
|
117
|
+
* discovered, so it is where the entry and its row are dropped and every
|
|
107
118
|
* rail told; the 404 then says what happened instead of looking like a
|
|
108
119
|
* crash (§5b). */
|
|
109
120
|
const ensureLoadable = async (id) => {
|
|
@@ -135,10 +146,14 @@ export function createServer({ factory, router, hub, sessions: state, config, pr
|
|
|
135
146
|
const sessions = await listSessions();
|
|
136
147
|
for (const s of sessions)
|
|
137
148
|
nascent.delete(s.id);
|
|
138
|
-
// A session created but never prompted would otherwise be listed forever
|
|
139
|
-
|
|
140
|
-
|
|
149
|
+
// A session created but never prompted would otherwise be listed forever —
|
|
150
|
+
// and, since creation ranks it, hold a working-set slot forever too.
|
|
151
|
+
for (const [id, n] of nascent) {
|
|
152
|
+
if (Date.now() - n.createdAt > 86_400_000) {
|
|
141
153
|
nascent.delete(id);
|
|
154
|
+
state.forget(id);
|
|
155
|
+
}
|
|
156
|
+
}
|
|
142
157
|
const owned = taskSessions?.() ?? new Set();
|
|
143
158
|
return [
|
|
144
159
|
...[...nascent].map(([id, n]) => ({ id, ...n })),
|
|
@@ -146,44 +161,56 @@ export function createServer({ factory, router, hub, sessions: state, config, pr
|
|
|
146
161
|
];
|
|
147
162
|
};
|
|
148
163
|
// One session as every list renders it: the summary, what the workbench
|
|
149
|
-
// decided about it, and what is true of it right now. `
|
|
150
|
-
//
|
|
164
|
+
// decided about it, and what is true of it right now. `rank` is the place in
|
|
165
|
+
// the rail's working set, when it has one; `modified` rides along for the
|
|
166
|
+
// row's tooltip, and orders nothing.
|
|
151
167
|
const present = (s, own, active) => ({
|
|
152
168
|
...s,
|
|
153
|
-
...(own?.
|
|
169
|
+
...(own?.rank === undefined ? {} : { rank: own.rank }),
|
|
154
170
|
state: router.stateOf(s.id) ?? "idle",
|
|
155
|
-
pinned: own?.pinned ?? false,
|
|
156
171
|
unread: own?.unread ?? false,
|
|
157
172
|
channel: channelOf?.(s.id) ?? "web",
|
|
158
173
|
activeRuns: active.get(s.id) ?? 0,
|
|
159
174
|
});
|
|
160
|
-
//
|
|
161
|
-
//
|
|
162
|
-
//
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
return c.json({ error: "sessions must be a list of ids" }, 400);
|
|
168
|
-
}
|
|
169
|
-
state.reorder(sessions);
|
|
170
|
-
hub.emitWorkspace({ type: "sessions-changed" });
|
|
171
|
-
return c.json({ ok: true });
|
|
175
|
+
// The rail's top rows are maintained here and nowhere else: a session a
|
|
176
|
+
// human speaks to — or creates, below — and that is not in the working set
|
|
177
|
+
// already takes the front slot (web/session-state.ts). No route — there is no
|
|
178
|
+
// gesture to make, and an IM message has no browser to make it from.
|
|
179
|
+
router.onSpokenTo((id) => {
|
|
180
|
+
if (state.promote(id))
|
|
181
|
+
hub.emitWorkspace({ type: "sessions-changed" });
|
|
172
182
|
});
|
|
173
183
|
app.get("/api/sessions", async (c) => {
|
|
174
184
|
const flags = state.flags();
|
|
175
185
|
const active = activeRuns();
|
|
176
186
|
return c.json((await allSessions()).map((s) => present(s, flags.get(s.id), active)));
|
|
177
187
|
});
|
|
188
|
+
// What was said, across every session: the palette's Messages section. The
|
|
189
|
+
// factory owns the index and the ranking; an empty query is an empty answer,
|
|
190
|
+
// not a listing of everything.
|
|
191
|
+
app.get("/api/search", async (c) => {
|
|
192
|
+
const q = (c.req.query("q") ?? "").trim();
|
|
193
|
+
return c.json({ hits: q ? await factory.search(q) : [] });
|
|
194
|
+
});
|
|
178
195
|
app.post("/api/sessions", async (c) => {
|
|
179
196
|
const body = await c.req.json().catch(() => ({}));
|
|
180
197
|
// A session always starts in its project directory — never in pier's own.
|
|
181
198
|
if (typeof body.cwd !== "string" || !body.cwd)
|
|
182
199
|
return c.json({ error: "cwd required" }, 400);
|
|
183
|
-
|
|
200
|
+
// The seam records the real path (agent/pi.ts), and the row below stands in
|
|
201
|
+
// for a listing until Pi persists the session — up to a day for one never
|
|
202
|
+
// prompted. Resolved here too, or that row is the one place a directory
|
|
203
|
+
// reached through a symlink still looks like a project of its own.
|
|
204
|
+
const cwd = await realpath(body.cwd).catch(() => body.cwd);
|
|
205
|
+
const session = await factory.create({ cwd });
|
|
184
206
|
const createdAt = Date.now();
|
|
185
|
-
nascent.set(session.id, { cwd
|
|
207
|
+
nascent.set(session.id, { cwd, createdAt });
|
|
186
208
|
router.attach({ channelId: "web", conversationId: session.id }, session);
|
|
209
|
+
// Created is as good as spoken to: the person who clicked New is about to
|
|
210
|
+
// type into it, and a row born below the working set would jump on the
|
|
211
|
+
// first message. Ghosts give the slot back (`ensureLoadable`, the expiry
|
|
212
|
+
// above).
|
|
213
|
+
state.promote(session.id);
|
|
187
214
|
hub.emitWorkspace({ type: "sessions-changed" });
|
|
188
215
|
return c.json({ id: session.id }, 201);
|
|
189
216
|
});
|
|
@@ -197,19 +224,6 @@ export function createServer({ factory, router, hub, sessions: state, config, pr
|
|
|
197
224
|
}
|
|
198
225
|
return c.json({ ok: true });
|
|
199
226
|
});
|
|
200
|
-
app.post("/api/sessions/:id/pin", async (c) => {
|
|
201
|
-
const body = await c.req.json().catch(() => null);
|
|
202
|
-
if (typeof body?.pinned !== "boolean")
|
|
203
|
-
return c.json({ error: "pinned required" }, 400);
|
|
204
|
-
const id = c.req.param("id");
|
|
205
|
-
// A pin is a row in the workbench's own table, so it has to be a session
|
|
206
|
-
// Pi knows — or one created here that Pi has not persisted yet.
|
|
207
|
-
if (!nascent.has(id) && !(await factory.find(id)))
|
|
208
|
-
return c.json({ error: `session ${id} not found` }, 404);
|
|
209
|
-
state.pin(id, body.pinned);
|
|
210
|
-
hub.emitWorkspace({ type: "sessions-changed" });
|
|
211
|
-
return c.json({ pinned: body.pinned });
|
|
212
|
-
});
|
|
213
227
|
// The two responses big enough to matter: a transcript, and one turn's tool
|
|
214
228
|
// detail. Scoped to these routes on purpose — compressing the SSE streams
|
|
215
229
|
// would sit on events until the encoder's buffer filled.
|
|
@@ -376,6 +390,11 @@ export function createServer({ factory, router, hub, sessions: state, config, pr
|
|
|
376
390
|
const session = await ensure(id);
|
|
377
391
|
if (session.state === "streaming")
|
|
378
392
|
return c.json({ error: "busy — stop the turn first" }, 409);
|
|
393
|
+
const latest = (await session.history()).filter((turn) => turn.role === "user").length - 1;
|
|
394
|
+
if (index !== latest)
|
|
395
|
+
return c.json({ error: "only the latest user message can be edited — refresh and try again" }, 409);
|
|
396
|
+
if (session.state !== "idle")
|
|
397
|
+
return c.json({ error: "busy — stop the turn first" }, 409);
|
|
379
398
|
await session.rewindToUserTurn(index);
|
|
380
399
|
// The rewind took the turns after this one out of the context, headers and
|
|
381
400
|
// all; what the model was told about who is speaking went with them.
|
|
@@ -452,7 +471,7 @@ export function createServer({ factory, router, hub, sessions: state, config, pr
|
|
|
452
471
|
return c.json({ ok: true }, 202);
|
|
453
472
|
});
|
|
454
473
|
// Workspace stream: one per client, keeps every session list in sync
|
|
455
|
-
// (created/
|
|
474
|
+
// (created/promoted → re-list, run state → patch) without polling.
|
|
456
475
|
app.get("/api/events", (c) => streamSSE(c, async (stream) => {
|
|
457
476
|
// A write to a torn-down stream must not become an unhandled rejection.
|
|
458
477
|
const unsubscribe = hub.subscribeWorkspace((e) => void stream.writeSSE({ data: JSON.stringify(e) }).catch(() => { }));
|