pleumrouter 0.5.48 → 0.5.49

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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "pleumrouter",
3
- "version": "0.5.48",
3
+ "version": "0.5.49",
4
4
  "description": "Official PleumRouter terminal CLI — launch any coding agent against PleumRouter.",
5
5
  "type": "module",
6
6
  "bin": {
@@ -102,6 +102,7 @@ export function rowsFor(s, width = terminalWidth()) {
102
102
  // 화살표 이동 · 타이핑 필터 · Enter 선택 · Esc/Ctrl+C 취소(null).
103
103
  // 비TTY면 즉시 null — 호출자가 기존 번호 피커 등으로 폴백한다.
104
104
  // process.exit는 절대 호출하지 않는다(취소 처리는 호출자 몫).
105
+ // `{ header: true }` 항목은 묶음 제목 — 커서가 건너뛰고 선택되지 않으며 필터 중엔 숨긴다.
105
106
  export async function select(items, {
106
107
  message = t("Select"),
107
108
  formatItem = String,
@@ -118,6 +119,16 @@ export async function select(items, {
118
119
  let cursor = Math.max(0, Math.min(initialCursor, Math.max(0, (items?.length ?? 1) - 1)));
119
120
  let windowStart = 0;
120
121
  let lastRows = 0;
122
+ const isHeader = (it) => it?.header === true;
123
+ const poolOf = () => {
124
+ const pool = matches(items, query);
125
+ return query ? pool.filter((it) => !isHeader(it)) : pool;
126
+ };
127
+ // from에서 dir 방향 첫 비제목 위치. 없으면 fallback(현재 위치 유지).
128
+ const seek = (pool, from, dir, fallback) => {
129
+ for (let i = from; i >= 0 && i < pool.length; i += dir) if (!isHeader(pool[i])) return i;
130
+ return fallback;
131
+ };
121
132
 
122
133
  const erase = () => {
123
134
  if (!lastRows) return;
@@ -126,8 +137,9 @@ export async function select(items, {
126
137
  };
127
138
 
128
139
  const render = () => {
129
- const pool = matches(items, query);
140
+ const pool = poolOf();
130
141
  if (cursor >= pool.length) cursor = Math.max(0, pool.length - 1);
142
+ if (isHeader(pool[cursor])) cursor = seek(pool, cursor, 1, seek(pool, cursor, -1, cursor));
131
143
  if (cursor < windowStart) {
132
144
  windowStart = cursor;
133
145
  } else if (cursor >= windowStart + limit) {
@@ -146,6 +158,7 @@ export async function select(items, {
146
158
  rawLines.push(
147
159
  ...shown.map((it, idx) => {
148
160
  const itemIndex = windowStart + idx;
161
+ if (isHeader(it)) return ` ${style.gray(formatItem(it))}`;
149
162
  return itemIndex === cursor
150
163
  ? `${style.plum("❯")} ${style.bold(formatItem(it))}`
151
164
  : ` ${style.dim(formatItem(it))}`;
@@ -180,15 +193,15 @@ export async function select(items, {
180
193
  for (const key of feed(chunk)) {
181
194
  if (key.type === "ctrlc" || key.type === "escape") return finish(null);
182
195
  if (key.type === "enter") {
183
- const pool = matches(items, query);
196
+ const pool = poolOf();
184
197
  const safeCursor = Math.max(0, Math.min(cursor, pool.length - 1));
198
+ if (isHeader(pool[safeCursor])) continue;
185
199
  return finish(pool[safeCursor] ?? null);
186
200
  }
187
201
  if (key.type === "up") {
188
- cursor = Math.max(0, cursor - 1);
202
+ cursor = seek(poolOf(), cursor - 1, -1, cursor);
189
203
  } else if (key.type === "down") {
190
- const pool = matches(items, query);
191
- cursor = Math.min(Math.max(0, pool.length - 1), cursor + 1);
204
+ cursor = seek(poolOf(), cursor + 1, 1, cursor);
192
205
  } else if (key.type === "backspace") {
193
206
  query = query.slice(0, -1);
194
207
  cursor = 0;
@@ -260,6 +260,7 @@ export const ES = {
260
260
  "Team": "Equipo",
261
261
  "Personal": "Personal",
262
262
  "Personal Credit": "Crédito personal",
263
+ "Invited workspaces": "Espacios de trabajo invitados",
263
264
  "Credit workspace": "Espacio de crédito",
264
265
  "Credit Balance:": "Saldo de crédito:",
265
266
  "credits": "créditos",
@@ -260,6 +260,7 @@ export const JA = {
260
260
  "Team": "チーム",
261
261
  "Personal": "個人",
262
262
  "Personal Credit": "個人クレジット",
263
+ "Invited workspaces": "招待されたワークスペース",
263
264
  "Credit workspace": "クレジットワークスペース",
264
265
  "Credit Balance:": "クレジット残高:",
265
266
  "credits": "クレジット",
@@ -319,6 +319,7 @@ export const KO = {
319
319
  "Team": "팀",
320
320
  "Personal": "개인",
321
321
  "Personal Credit": "개인 크레딧",
322
+ "Invited workspaces": "초대받은 워크스페이스",
322
323
  "Credit workspace": "크레딧 워크스페이스",
323
324
  "Credit Balance:": "크레딧 잔액:",
324
325
  "credits": "크레딧",
@@ -260,6 +260,7 @@ export const ZH = {
260
260
  "Team": "团队",
261
261
  "Personal": "个人",
262
262
  "Personal Credit": "个人额度",
263
+ "Invited workspaces": "受邀的工作区",
263
264
  "Credit workspace": "额度工作区",
264
265
  "Credit Balance:": "额度余额:",
265
266
  "credits": "额度",
package/src/workspace.mjs CHANGED
@@ -56,15 +56,24 @@ export function workspaceLabel(context) {
56
56
  return t("Personal");
57
57
  }
58
58
 
59
+ // 서버가 소유 공간(role=owner)을 먼저 보낸다. 소유=개인 크레딧 아래, 나머지=초대받은 공간 묶음.
60
+ export function splitCandidates(candidates = []) {
61
+ return {
62
+ owned: candidates.filter((c) => c.role === "owner"),
63
+ invited: candidates.filter((c) => c.role !== "owner"),
64
+ };
65
+ }
66
+
59
67
  export function formatWorkspaceList(context, candidates = []) {
60
68
  const activeId = context?.type === "workspace" ? String(context.workspace_id) : null;
61
- const lines = [`${t("Credit workspace")}: ${workspaceLabel(context)}`];
62
- for (const candidate of candidates) {
69
+ const { owned, invited } = splitCandidates(candidates);
70
+ const line = (candidate) => {
63
71
  const mark = String(candidate.workspace_id) === activeId ? style.bold(style.plum("▸ ")) : " ";
64
- lines.push(
65
- `${mark}${candidate.workspace_id} ${sanitizeTerminalText(candidate.name)} (${sanitizeTerminalText(candidate.role)})`,
66
- );
67
- }
72
+ return `${mark}${candidate.workspace_id} ${sanitizeTerminalText(candidate.name)} (${sanitizeTerminalText(candidate.role)})`;
73
+ };
74
+ const lines = [`${t("Credit workspace")}: ${workspaceLabel(context)}`];
75
+ if (owned.length) lines.push(`${t("Personal Credit")}:`, ...owned.map(line));
76
+ if (invited.length) lines.push(`${t("Invited workspaces")}:`, ...invited.map(line));
68
77
  return lines.join("\n");
69
78
  }
70
79
 
@@ -118,6 +127,15 @@ export async function selectAndBindWorkspace({
118
127
  const active = data?.active_context ?? getActiveContext();
119
128
  const activeId = active?.type === "workspace" ? String(active.workspace_id) : null;
120
129
 
130
+ const { owned, invited } = splitCandidates(candidates);
131
+ const option = (c, title) => ({
132
+ id: String(c.workspace_id),
133
+ type: "workspace",
134
+ workspace_id: c.workspace_id,
135
+ name: c.name,
136
+ title,
137
+ label: `${activeId === String(c.workspace_id) ? "✓ " : " "} └ ${title}`,
138
+ });
121
139
  const options = [
122
140
  {
123
141
  id: "personal",
@@ -125,20 +143,17 @@ export async function selectAndBindWorkspace({
125
143
  title: t("Personal Credit"),
126
144
  label: `${activeId ? " " : "✓ "}${t("Personal Credit")}`,
127
145
  },
128
- ...candidates.map((c) => ({
129
- id: String(c.workspace_id),
130
- type: "workspace",
131
- workspace_id: c.workspace_id,
132
- name: c.name,
133
- title: `${sanitizeTerminalText(c.name)} (${sanitizeTerminalText(c.role)})`,
134
- label: `${activeId === String(c.workspace_id) ? "✓ " : " "}${sanitizeTerminalText(c.name)} (${sanitizeTerminalText(c.role)})`,
135
- })),
146
+ ...owned.map((c) => option(c, sanitizeTerminalText(c.name))),
147
+ ...(invited.length
148
+ ? [{ id: "header:invited", header: true, label: ` ${t("Invited workspaces")}` }]
149
+ : []),
150
+ ...invited.map((c) => option(c, `${sanitizeTerminalText(c.name)} (${sanitizeTerminalText(c.role)})`)),
136
151
  ];
137
152
 
138
153
  const picked = await selectFn(options, {
139
154
  message: t("Select billing workspace for this agent"),
140
155
  formatItem: (x) => x.label,
141
- limit: 10,
156
+ limit: 12,
142
157
  });
143
158
 
144
159
  if (!picked) return active;