@poncho-ai/cli 0.37.0 → 0.38.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.
@@ -631,7 +631,7 @@ export const WEB_UI_STYLES = `
631
631
  /* Messages */
632
632
  .messages { flex: 1; overflow-y: auto; overflow-x: hidden; padding: 24px 24px; }
633
633
  .messages-column { max-width: 680px; margin: 0 auto; }
634
- .message-row { margin-bottom: 24px; display: flex; max-width: 100%; }
634
+ .message-row { margin-bottom: 24px; display: flex; max-width: 100%; position: relative; }
635
635
  .message-row.user { justify-content: flex-end; }
636
636
  .assistant-wrap { display: flex; gap: 12px; width: 100%; min-width: 0; }
637
637
  .assistant-avatar {
@@ -1990,6 +1990,283 @@ export const WEB_UI_STYLES = `
1990
1990
  background: var(--surface-1);
1991
1991
  }
1992
1992
 
1993
+ /* Thread affordances rendered as their own block in the messages column,
1994
+ tucked tight beneath the parent message — Slack-style indicator. */
1995
+ .thread-affordance-block {
1996
+ display: flex;
1997
+ flex-direction: column;
1998
+ gap: 0;
1999
+ /* Negative top margin compensates for .message-row's 24px bottom
2000
+ margin so the indicator sits right under the message it belongs to. */
2001
+ margin: -18px 0 8px 30px;
2002
+ }
2003
+ .message-row.user + .thread-affordance-block {
2004
+ margin-left: 0;
2005
+ margin-right: 12px;
2006
+ align-items: flex-end;
2007
+ }
2008
+ .thread-row {
2009
+ display: flex;
2010
+ align-items: center;
2011
+ gap: 6px;
2012
+ padding: 4px 6px 4px 8px;
2013
+ border: 0;
2014
+ background: transparent;
2015
+ font: inherit;
2016
+ color: var(--fg-3);
2017
+ cursor: pointer;
2018
+ text-align: left;
2019
+ border-radius: 8px;
2020
+ transition: color 0.1s, background 0.1s;
2021
+ width: 100%;
2022
+ max-width: 100%;
2023
+ }
2024
+ .thread-row:hover {
2025
+ color: var(--fg);
2026
+ background: var(--bg-bubble-user, rgba(0,0,0,0.04));
2027
+ }
2028
+ /* Under user messages, reuse the "Reply in thread" pill design. */
2029
+ .message-row.user + .thread-affordance-block .thread-row {
2030
+ width: auto;
2031
+ max-width: 100%;
2032
+ padding: 4px 6px 4px 12px;
2033
+ border: 1px solid var(--border, rgba(0,0,0,0.12));
2034
+ background: var(--bg, #fff);
2035
+ border-radius: 999px;
2036
+ box-shadow: 0 1px 3px rgba(0,0,0,0.06);
2037
+ }
2038
+ .message-row.user + .thread-affordance-block .thread-row:hover {
2039
+ background: var(--bg, #fff);
2040
+ border-color: var(--accent, var(--border));
2041
+ color: var(--accent, var(--fg));
2042
+ }
2043
+ .message-row.user + .thread-affordance-block .thread-row-count {
2044
+ color: inherit;
2045
+ }
2046
+ .thread-row-count {
2047
+ font-size: 13px;
2048
+ font-weight: 500;
2049
+ color: var(--accent, #3b82f6);
2050
+ }
2051
+ .thread-row-meta {
2052
+ font-size: 12px;
2053
+ color: var(--fg-3);
2054
+ }
2055
+ .thread-row-delete {
2056
+ margin-left: auto;
2057
+ padding: 0 6px;
2058
+ border: 0;
2059
+ background: transparent;
2060
+ color: var(--fg-7, var(--fg-3));
2061
+ cursor: pointer;
2062
+ font-size: 16px;
2063
+ line-height: 1;
2064
+ opacity: 0;
2065
+ transition: opacity 0.15s, color 0.15s;
2066
+ }
2067
+ .thread-row:hover .thread-row-delete,
2068
+ .thread-pill-pair:hover .thread-row-delete,
2069
+ .reply-pill-wrap:hover .thread-row-delete {
2070
+ opacity: 1;
2071
+ }
2072
+ .thread-row-delete:hover {
2073
+ color: var(--fg-2);
2074
+ background: transparent;
2075
+ }
2076
+
2077
+ /* Reply-in-thread pill — absolutely positioned so it floats below the
2078
+ message row without pushing content. Position differs by role. */
2079
+ .reply-pill-wrap {
2080
+ position: absolute;
2081
+ z-index: 5;
2082
+ display: inline-flex;
2083
+ align-items: center;
2084
+ gap: 4px;
2085
+ opacity: 0;
2086
+ visibility: hidden;
2087
+ /* Hide is delayed so the user has time to move the mouse from the
2088
+ message bubble down to the pill without the pill disappearing or
2089
+ losing interactivity in the gap between them. */
2090
+ transition: opacity 0.12s 0.2s, visibility 0s 0.32s;
2091
+ /* Default: assistant placement (overridden for user below). */
2092
+ bottom: -36px;
2093
+ left: 36px;
2094
+ right: auto;
2095
+ }
2096
+ .message-row.user .reply-pill-wrap {
2097
+ bottom: -20px;
2098
+ right: 0;
2099
+ left: auto;
2100
+ }
2101
+ .message-row:hover .reply-pill-wrap,
2102
+ .reply-pill-wrap:hover {
2103
+ opacity: 1;
2104
+ visibility: visible;
2105
+ transition: opacity 0.12s 0s, visibility 0s 0s;
2106
+ }
2107
+ /* When the message has at least one thread, the badge stays visible at
2108
+ all times (no hover required) and stacks vertically when there are
2109
+ multiple threads. */
2110
+ .reply-pill-wrap.has-threads {
2111
+ opacity: 1;
2112
+ visibility: visible;
2113
+ flex-direction: column;
2114
+ align-items: flex-start;
2115
+ gap: 4px;
2116
+ transition: none;
2117
+ }
2118
+ .message-row.user .reply-pill-wrap.has-threads {
2119
+ align-items: flex-end;
2120
+ }
2121
+ .thread-pill-pair {
2122
+ position: relative;
2123
+ display: inline-flex;
2124
+ align-items: center;
2125
+ }
2126
+ .thread-pill-pair .thread-row-delete {
2127
+ position: absolute;
2128
+ left: 100%;
2129
+ top: 0;
2130
+ bottom: 0;
2131
+ margin: 0 0 0 4px;
2132
+ padding: 0 6px;
2133
+ display: grid;
2134
+ place-items: center;
2135
+ background: transparent;
2136
+ }
2137
+ .reply-icon-btn {
2138
+ display: inline-flex;
2139
+ align-items: center;
2140
+ gap: 4px;
2141
+ padding: 4px 10px;
2142
+ border: 1px solid var(--border, rgba(0,0,0,0.12));
2143
+ background: var(--bg, #fff);
2144
+ cursor: pointer;
2145
+ color: var(--fg-3);
2146
+ font-size: 12px;
2147
+ border-radius: 999px;
2148
+ box-shadow: 0 1px 3px rgba(0,0,0,0.06);
2149
+ transition: color 0.1s, background 0.1s, border-color 0.1s;
2150
+ }
2151
+ .reply-icon-btn:hover {
2152
+ color: var(--accent, var(--fg));
2153
+ border-color: var(--accent, var(--border));
2154
+ /* Keep an opaque background — the var(--bg-bubble-user) fallback can
2155
+ be transparent in some themes which makes the pill see-through. */
2156
+ background: var(--bg, #fff);
2157
+ }
2158
+ .reply-icon-btn svg { width: 13px; height: 13px; }
2159
+ .thread-pill .thread-pill-count {
2160
+ font-weight: 500;
2161
+ }
2162
+ .thread-pill .thread-pill-meta {
2163
+ margin-left: 2px;
2164
+ color: var(--fg-3);
2165
+ font-weight: 400;
2166
+ }
2167
+
2168
+ /* Thread panel — flex sibling matching the browser-panel pattern */
2169
+ .thread-panel-resize {
2170
+ width: 1px;
2171
+ cursor: col-resize;
2172
+ background: var(--border-1);
2173
+ flex-shrink: 0;
2174
+ position: relative;
2175
+ z-index: 10;
2176
+ }
2177
+ .thread-panel-resize::after {
2178
+ content: "";
2179
+ position: absolute;
2180
+ inset: 0 -3px;
2181
+ }
2182
+ .thread-panel-resize:hover,
2183
+ .thread-panel-resize.dragging {
2184
+ background: var(--fg-5);
2185
+ }
2186
+ .thread-panel {
2187
+ flex: 1 1 0%;
2188
+ min-width: 320px;
2189
+ background: var(--bg);
2190
+ display: flex;
2191
+ flex-direction: column;
2192
+ overflow: hidden;
2193
+ }
2194
+ .main-chat.has-thread {
2195
+ flex: 1 1 0%;
2196
+ min-width: 280px;
2197
+ }
2198
+ .thread-panel-header {
2199
+ display: flex;
2200
+ align-items: center;
2201
+ justify-content: space-between;
2202
+ gap: 8px;
2203
+ padding: 8px 12px;
2204
+ border-bottom: 1px solid var(--border);
2205
+ min-height: 40px;
2206
+ }
2207
+ .thread-panel-title {
2208
+ font-size: 12px;
2209
+ font-weight: 600;
2210
+ text-transform: uppercase;
2211
+ letter-spacing: 0.06em;
2212
+ color: var(--fg-tool);
2213
+ white-space: nowrap;
2214
+ }
2215
+ .thread-panel-close {
2216
+ background: none;
2217
+ border: none;
2218
+ color: var(--fg-3);
2219
+ font-size: 18px;
2220
+ cursor: pointer;
2221
+ width: 28px;
2222
+ height: 28px;
2223
+ padding: 0;
2224
+ display: grid;
2225
+ place-items: center;
2226
+ line-height: 1;
2227
+ border-radius: 4px;
2228
+ }
2229
+ .thread-panel-close:hover { color: var(--fg); }
2230
+ .thread-panel-parent {
2231
+ padding: 12px 16px;
2232
+ border-bottom: 1px solid var(--border);
2233
+ background: var(--bg-bubble-user, rgba(0,0,0,0.02));
2234
+ font-size: 13px;
2235
+ }
2236
+ .thread-panel-parent .message-row {
2237
+ margin: 0;
2238
+ }
2239
+ .thread-panel-parent-empty {
2240
+ color: var(--fg-3);
2241
+ font-style: italic;
2242
+ }
2243
+ .thread-panel-messages {
2244
+ flex: 1;
2245
+ overflow-y: auto;
2246
+ padding: 12px 16px;
2247
+ }
2248
+ /* Match the main composer's vertical padding so both chatboxes align
2249
+ at the same baseline. Horizontal padding is reduced because the panel
2250
+ is narrower than the main pane. */
2251
+ .thread-composer {
2252
+ padding: 12px 12px 24px;
2253
+ }
2254
+ .thread-composer .composer-inner {
2255
+ margin: 0;
2256
+ }
2257
+ /* Thread-row delete button — matches the sidebar conversation-item delete UX */
2258
+ .thread-row-delete.confirming {
2259
+ opacity: 1 !important;
2260
+ padding: 0 8px;
2261
+ font-size: 11px;
2262
+ color: var(--error, #e44);
2263
+ background: transparent;
2264
+ }
2265
+ .thread-row-delete.confirming:hover {
2266
+ color: var(--error-alt, #c33);
2267
+ background: transparent;
2268
+ }
2269
+
1993
2270
  /* Reduced motion */
1994
2271
  @media (prefers-reduced-motion: reduce) {
1995
2272
  *, *::before, *::after {
package/src/web-ui.ts CHANGED
@@ -205,6 +205,30 @@ ${WEB_UI_STYLES}
205
205
  <div id="browser-panel-placeholder" class="browser-panel-placeholder">No active browser session</div>
206
206
  </div>
207
207
  </aside>
208
+ <div id="thread-panel-resize" class="thread-panel-resize" style="display:none"></div>
209
+ <aside id="thread-panel" class="thread-panel" style="display:none">
210
+ <div class="thread-panel-header">
211
+ <span class="thread-panel-title">Thread</span>
212
+ <button id="thread-panel-close" class="thread-panel-close" title="Close thread">&times;</button>
213
+ </div>
214
+ <div id="thread-panel-parent" class="thread-panel-parent"></div>
215
+ <div id="thread-panel-messages" class="thread-panel-messages messages"></div>
216
+ <form id="thread-composer" class="composer thread-composer">
217
+ <div class="composer-inner">
218
+ <div id="thread-attachment-preview" class="attachment-preview" style="display:none"></div>
219
+ <div class="composer-shell">
220
+ <button id="thread-attach-btn" class="attach-btn" type="button" title="Attach files">
221
+ <svg width="16" height="16" viewBox="0 0 16 16" fill="none"><path d="M8 3v10M3 8h10" stroke="currentColor" stroke-width="2" stroke-linecap="round"/></svg>
222
+ </button>
223
+ <input id="thread-file-input" type="file" multiple accept="image/*,video/*,application/pdf,.txt,.csv,.json,.html" style="display:none" />
224
+ <textarea id="thread-prompt" class="composer-input" placeholder="Reply in thread..." rows="1"></textarea>
225
+ <button id="thread-send" class="send-btn" type="submit">
226
+ <svg width="16" height="16" viewBox="0 0 16 16" fill="none"><path d="M8 12V4M4 7l4-4 4 4" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/></svg>
227
+ </button>
228
+ </div>
229
+ </div>
230
+ </form>
231
+ </aside>
208
232
  </div>
209
233
  </main>
210
234
  </div>
package/test/cli.test.ts CHANGED
@@ -14,7 +14,7 @@ import {
14
14
  vi.mock("@poncho-ai/harness", async (importOriginal) => {
15
15
  const actual = await importOriginal<typeof import("@poncho-ai/harness")>();
16
16
  return {
17
- parseAgentMarkdown: actual.parseAgentMarkdown,
17
+ ...actual,
18
18
  AgentHarness: class MockHarness {
19
19
  async initialize(): Promise<void> {}
20
20
  setSubagentManager(): void {}
@@ -140,14 +140,65 @@ vi.mock("@poncho-ai/harness", async (importOriginal) => {
140
140
  },
141
141
  createConversationStore: () => {
142
142
  const store = new FileConversationStore(process.cwd());
143
+ // The test mock conversation store implements the minimum of the
144
+ // ConversationStore interface the CLI uses. The file store already
145
+ // holds the full conversation object, so the light `get` / heavy
146
+ // `getWithArchive` distinction doesn't matter here — both delegate
147
+ // to the same underlying read.
148
+ // FileConversationStore returns a narrow WebUiConversation; widen to
149
+ // the optional harness fields so the summary / snapshot projections
150
+ // can reference them without TS errors.
151
+ type LooseConv = Awaited<ReturnType<FileConversationStore["get"]>> & {
152
+ parentConversationId?: string;
153
+ pendingApprovals?: unknown[];
154
+ channelMeta?: { platform: string; channelId: string; platformThreadId: string };
155
+ _continuationMessages?: unknown[];
156
+ runStatus?: "running" | "idle";
157
+ };
158
+ const listSummaries = async (ownerId?: string) =>
159
+ ((await store.list(ownerId)) as LooseConv[]).map((c) => ({
160
+ conversationId: c!.conversationId,
161
+ title: c!.title,
162
+ updatedAt: c!.updatedAt,
163
+ createdAt: c!.createdAt,
164
+ ownerId: c!.ownerId,
165
+ tenantId: c!.tenantId,
166
+ parentConversationId: c!.parentConversationId,
167
+ messageCount: c!.messages?.length ?? 0,
168
+ hasPendingApprovals:
169
+ Array.isArray(c!.pendingApprovals) && c!.pendingApprovals.length > 0,
170
+ channelMeta: c!.channelMeta,
171
+ }));
143
172
  return {
144
173
  list: (ownerId?: string) => store.list(ownerId),
174
+ listSummaries,
145
175
  get: (conversationId: string) => store.get(conversationId),
176
+ getWithArchive: (conversationId: string) => store.get(conversationId),
177
+ getStatusSnapshot: async (conversationId: string) => {
178
+ const c = (await store.get(conversationId)) as LooseConv | undefined;
179
+ if (!c) return undefined;
180
+ return {
181
+ conversationId: c.conversationId,
182
+ updatedAt: c.updatedAt,
183
+ messageCount: c.messages?.length ?? 0,
184
+ hasPendingApprovals:
185
+ Array.isArray(c.pendingApprovals) && c.pendingApprovals.length > 0,
186
+ hasContinuationMessages:
187
+ Array.isArray(c._continuationMessages) &&
188
+ (c._continuationMessages?.length ?? 0) > 0,
189
+ parentConversationId: c.parentConversationId ?? null,
190
+ ownerId: c.ownerId,
191
+ tenantId: c.tenantId ?? null,
192
+ runStatus: c.runStatus ?? null,
193
+ };
194
+ },
146
195
  create: (ownerId?: string, title?: string) => store.create(ownerId, title),
147
196
  update: (conversation: Awaited<ReturnType<FileConversationStore["create"]>>) =>
148
197
  store.update(conversation),
149
198
  rename: (conversationId: string, title: string) => store.rename(conversationId, title),
150
199
  delete: (conversationId: string) => store.delete(conversationId),
200
+ appendSubagentResult: async () => {},
201
+ clearCallbackLock: async (conversationId: string) => store.get(conversationId),
151
202
  };
152
203
  },
153
204
  InMemoryStateStore: class {},